diff --git a/static/app/components/events/autofix/autofixActionSelector.tsx b/static/app/components/events/autofix/autofixActionSelector.tsx index 6432bdc9a8627b..7342234f05305f 100644 --- a/static/app/components/events/autofix/autofixActionSelector.tsx +++ b/static/app/components/events/autofix/autofixActionSelector.tsx @@ -83,6 +83,7 @@ function AutofixActionSelector<T extends string>({ const Container = styled('div')` min-height: 40px; + padding: 0 ${space(1)}; `; const ContentWrapper = styled('div')` diff --git a/static/app/components/events/autofix/autofixChanges.tsx b/static/app/components/events/autofix/autofixChanges.tsx index 014e76a2a5c8cf..4050f5b8447e8e 100644 --- a/static/app/components/events/autofix/autofixChanges.tsx +++ b/static/app/components/events/autofix/autofixChanges.tsx @@ -1,21 +1,29 @@ -import {Fragment} from 'react'; +import React, {Fragment, useEffect, useState} from 'react'; import styled from '@emotion/styled'; import {AnimatePresence, type AnimationProps, motion} from 'framer-motion'; import ClippedBox from 'sentry/components/clippedBox'; import {AutofixDiff} from 'sentry/components/events/autofix/autofixDiff'; -import type { - AutofixChangesStep, - AutofixCodebaseChange, +import {SetupAndCreatePRsButton} from 'sentry/components/events/autofix/autofixPrButton'; +import {AutofixViewPrButton} from 'sentry/components/events/autofix/autofixViewPrButton'; +import { + type AutofixChangesStep, + type AutofixCodebaseChange, + AutofixStatus, } from 'sentry/components/events/autofix/types'; import {useAutofixData} from 'sentry/components/events/autofix/useAutofix'; -import {IconFix} from 'sentry/icons'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; +import {IconChevron, IconFix} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import testableTransition from 'sentry/utils/testableTransition'; +import usePrevious from 'sentry/utils/usePrevious'; type AutofixChangesProps = { + changesVersionIndex: number; groupId: string; + hasMoreThanOneChangesStep: boolean; + hasStepBelow: boolean; runId: string; step: AutofixChangesStep; }; @@ -30,12 +38,19 @@ function AutofixRepoChange({ runId: string; }) { return ( - <Content> + <RepoChangeContent> <RepoChangesHeader> <div> <Title>{change.title}</Title> <PullRequestTitle>{change.repo_name}</PullRequestTitle> </div> + {change.pull_request && ( + <AutofixViewPrButton + repoName={change.repo_name} + prUrl={change.pull_request?.pr_url} + isPrimary={false} + /> + )} </RepoChangesHeader> <AutofixDiff diff={change.diff} @@ -44,7 +59,7 @@ function AutofixRepoChange({ repoId={change.repo_external_id} editable={!change.pull_request} /> - </Content> + </RepoChangeContent> ); } @@ -69,12 +84,29 @@ const cardAnimationProps: AnimationProps = { }), }; -export function AutofixChanges({step, groupId, runId}: AutofixChangesProps) { +export function AutofixChanges({ + step, + groupId, + runId, + hasStepBelow, + changesVersionIndex, + hasMoreThanOneChangesStep, +}: AutofixChangesProps) { const data = useAutofixData({groupId}); + const [isExpanded, setIsExpanded] = useState(!hasStepBelow); + + const previousHasStepBelow = usePrevious(hasStepBelow); + + useEffect(() => { + // When a new step shows up below this one, we want to auto collapse it. + if (previousHasStepBelow && !hasStepBelow) { + setIsExpanded(false); + } + }, [previousHasStepBelow, hasStepBelow]); if (step.status === 'ERROR' || data?.status === 'ERROR') { return ( - <Content> + <RepoChangeContent> <PreviewContent> {data?.error_message ? ( <Fragment> @@ -85,38 +117,93 @@ export function AutofixChanges({step, groupId, runId}: AutofixChangesProps) { <span>{t('Something went wrong.')}</span> )} </PreviewContent> - </Content> + </RepoChangeContent> ); } - if (!step.changes.length) { + if ( + step.status === AutofixStatus.COMPLETED && + Object.keys(step.codebase_changes).length === 0 + ) { return ( - <Content> + <RepoChangeContent> <PreviewContent> <span>{t('Could not find a fix.')}</span> </PreviewContent> - </Content> + </RepoChangeContent> ); } - const allChangesHavePullRequests = step.changes.every(change => change.pull_request); + const allChangesHavePullRequests = Object.values(step.codebase_changes).every( + change => change.pull_request + ); + + const changesText = hasMoreThanOneChangesStep + ? hasStepBelow + ? t('Changes (version %s)', changesVersionIndex + 1) + : t('Latest Changes (version %s)', changesVersionIndex + 1) + : t('Changes'); + + const changesAreReady = Object.values(step.codebase_changes).every( + change => change.diff_str + ); return ( <AnimatePresence initial> <AnimationWrapper key="card" {...cardAnimationProps}> - <ChangesContainer allChangesHavePullRequests={allChangesHavePullRequests}> - <ClippedBox clipHeight={408}> - <HeaderText> - <IconFix size="sm" /> - {t('Fixes')} - </HeaderText> - {step.changes.map((change, i) => ( - <Fragment key={change.repo_external_id}> - {i > 0 && <Separator />} - <AutofixRepoChange change={change} groupId={groupId} runId={runId} /> - </Fragment> - ))} - </ClippedBox> + <ChangesContainer + allChangesHavePullRequests={allChangesHavePullRequests} + hasStepBelow={hasStepBelow} + > + <HeaderRow + onClick={() => hasStepBelow && setIsExpanded(!isExpanded)} + role={hasStepBelow ? 'button' : undefined} + aria-expanded={hasStepBelow ? isExpanded : undefined} + expandable={hasStepBelow} + > + <HeaderTextWrapper> + <HeaderText isPreviousChanges={hasStepBelow}> + <IconFix size="sm" /> + {changesText} + </HeaderText> + </HeaderTextWrapper> + <HeaderButtonsWrapper> + {changesAreReady ? ( + <React.Fragment> + {(!hasStepBelow || isExpanded) && !allChangesHavePullRequests && ( + <SetupAndCreatePRsButton + changes={Object.values(step.codebase_changes)} + groupId={groupId} + hasStepBelow={hasStepBelow} + changesStepId={step.id} + isPrimary={false} + /> + )} + {hasStepBelow && ( + <CollapseButton + aria-label={ + isExpanded ? t('Collapse changes') : t('Expand changes') + } + > + <IconChevron direction={isExpanded ? 'up' : 'down'} size="sm" /> + </CollapseButton> + )} + </React.Fragment> + ) : ( + <StyledLoadingIndicator size={14} mini hideMessage /> + )} + </HeaderButtonsWrapper> + </HeaderRow> + {(!hasStepBelow || isExpanded) && changesAreReady && ( + <ClippedBox clipHeight={408}> + {Object.values(step.codebase_changes).map((change, i) => ( + <Fragment key={change.repo_external_id}> + {i > 0 && <Separator />} + <AutofixRepoChange change={change} groupId={groupId} runId={runId} /> + </Fragment> + ))} + </ClippedBox> + )} </ChangesContainer> </AnimationWrapper> </AnimatePresence> @@ -136,21 +223,24 @@ const AnimationWrapper = styled(motion.div)` const PrefixText = styled('span')``; -const ChangesContainer = styled('div')<{allChangesHavePullRequests: boolean}>` - border: 2px solid +const ChangesContainer = styled('div')<{ + allChangesHavePullRequests: boolean; + hasStepBelow?: boolean; +}>` + border: ${p => (p.hasStepBelow ? 1 : 2)}px solid ${p => - p.allChangesHavePullRequests - ? p.theme.alert.success.border - : p.theme.alert.info.border}; + p.hasStepBelow + ? p.theme.innerBorder + : p.allChangesHavePullRequests + ? p.theme.alert.success.border + : p.theme.alert.info.border}; border-radius: ${p => p.theme.borderRadius}; box-shadow: ${p => p.theme.dropShadowMedium}; - padding-left: ${space(2)}; - padding-right: ${space(2)}; - padding-top: ${space(1)}; + overflow: hidden; `; -const Content = styled('div')` - padding: 0 ${space(1)} ${space(1)} ${space(1)}; +const RepoChangeContent = styled('div')` + padding: 0 ${space(2)} ${space(2)} ${space(2)}; `; const Title = styled('div')` @@ -164,9 +254,10 @@ const PullRequestTitle = styled('div')` const RepoChangesHeader = styled('div')` padding: ${space(2)} 0; - display: grid; - align-items: center; - grid-template-columns: 1fr auto; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: ${space(2)}; `; const Separator = styled('hr')` @@ -175,10 +266,61 @@ const Separator = styled('hr')` margin: ${space(2)} -${space(2)} 0 -${space(2)}; `; -const HeaderText = styled('div')` +const HeaderRow = styled('div')<{expandable?: boolean}>` + display: flex; + align-items: center; + width: 100%; + justify-content: space-between; + gap: ${space(1)}; + height: calc((2 * ${space(2)}) + ${p => p.theme.form.sm.height}px); + padding: ${space(2)}; + cursor: ${p => (p.expandable ? 'pointer' : 'default')}; + + &:hover { + background-color: ${p => + p.expandable ? p.theme.backgroundSecondary : 'transparent'}; + } +`; + +const HeaderButtonsWrapper = styled('div')` + display: flex; + align-items: center; + gap: ${space(2)}; +`; + +const StyledLoadingIndicator = styled(LoadingIndicator)` + && { + margin: 0; + height: 16px; + width: 16px; + } +`; + +const HeaderText = styled('div')<{isPreviousChanges?: boolean}>` font-weight: bold; - font-size: 1.2em; + font-size: ${p => (p.isPreviousChanges ? 1.1 : 1.2)}em; + color: ${p => (p.isPreviousChanges ? p.theme.subText : p.theme.textColor)}; display: flex; align-items: center; gap: ${space(1)}; `; + +const HeaderTextWrapper = styled('div')` + display: flex; + align-items: center; + gap: ${space(1)}; +`; + +const CollapseButton = styled('button')` + background: none; + border: none; + cursor: pointer; + padding: 0; + color: ${p => p.theme.subText}; + display: flex; + align-items: center; + + &:hover { + color: ${p => p.theme.textColor}; + } +`; diff --git a/static/app/components/events/autofix/autofixInsightCards.tsx b/static/app/components/events/autofix/autofixInsightCards.tsx index 1d185dd5d314ff..892bd3f0d09747 100644 --- a/static/app/components/events/autofix/autofixInsightCards.tsx +++ b/static/app/components/events/autofix/autofixInsightCards.tsx @@ -412,6 +412,39 @@ export function useUpdateInsightCard({groupId, runId}: {groupId: string; runId: }); } +export function useSendFeedbackOnChanges({ + groupId, + runId, +}: { + groupId: string; + runId: string; +}) { + const api = useApi({persistInFlight: true}); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (params: {message: string}) => { + return api.requestPromise(`/issues/${groupId}/autofix/update/`, { + method: 'POST', + data: { + run_id: runId, + payload: { + type: 'continue_with_feedback', + message: params.message, + }, + }, + }); + }, + onSuccess: _ => { + queryClient.invalidateQueries({queryKey: makeAutofixQueryKey(groupId)}); + addSuccessMessage(t('Thanks, rethinking this...')); + }, + onError: () => { + addErrorMessage(t('Something went wrong when sending Autofix your message.')); + }, + }); +} + function ChainLink({ groupId, runId, diff --git a/static/app/components/events/autofix/autofixMessageBox.tsx b/static/app/components/events/autofix/autofixMessageBox.tsx index fe8106687d5ba8..9dd7bf4a283b2e 100644 --- a/static/app/components/events/autofix/autofixMessageBox.tsx +++ b/static/app/components/events/autofix/autofixMessageBox.tsx @@ -3,22 +3,16 @@ import styled from '@emotion/styled'; import {AnimatePresence, type AnimationProps, motion} from 'framer-motion'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; -import {openModal} from 'sentry/actionCreators/modal'; import {Button, LinkButton} from 'sentry/components/button'; import AutofixActionSelector from 'sentry/components/events/autofix/autofixActionSelector'; import AutofixFeedback from 'sentry/components/events/autofix/autofixFeedback'; -import {AutofixSetupWriteAccessModal} from 'sentry/components/events/autofix/autofixSetupWriteAccessModal'; +import {SetupAndCreatePRsButton} from 'sentry/components/events/autofix/autofixPrButton'; import { - type AutofixCodebaseChange, AutofixStatus, type AutofixStep, AutofixStepType, } from 'sentry/components/events/autofix/types'; -import { - makeAutofixQueryKey, - useAutofixData, -} from 'sentry/components/events/autofix/useAutofix'; -import {useAutofixSetup} from 'sentry/components/events/autofix/useAutofixSetup'; +import {makeAutofixQueryKey} from 'sentry/components/events/autofix/useAutofix'; import Input from 'sentry/components/input'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {ScrollCarousel} from 'sentry/components/scrollCarousel'; @@ -81,102 +75,6 @@ interface AutofixMessageBoxProps { scrollText?: string; } -function CreatePRsButton({ - changes, - groupId, -}: { - changes: AutofixCodebaseChange[]; - groupId: string; -}) { - const autofixData = useAutofixData({groupId}); - const api = useApi(); - const queryClient = useQueryClient(); - const [hasClickedCreatePr, setHasClickedCreatePr] = useState(false); - - const createPRs = () => { - setHasClickedCreatePr(true); - for (const change of changes) { - createPr({change}); - } - }; - - const {mutate: createPr} = useMutation({ - mutationFn: ({change}: {change: AutofixCodebaseChange}) => { - return api.requestPromise(`/issues/${groupId}/autofix/update/`, { - method: 'POST', - data: { - run_id: autofixData?.run_id, - payload: { - type: 'create_pr', - repo_external_id: change.repo_external_id, - repo_id: change.repo_id, // The repo_id is only here for temporary backwards compatibility for LA customers, and we should remove it soon. - }, - }, - }); - }, - onSuccess: () => { - addSuccessMessage(t('Created pull requests.')); - queryClient.invalidateQueries({queryKey: makeAutofixQueryKey(groupId)}); - }, - onError: () => { - setHasClickedCreatePr(false); - addErrorMessage(t('Failed to create a pull request')); - }, - }); - - return ( - <Button - priority="primary" - onClick={createPRs} - icon={ - hasClickedCreatePr && <ProcessingStatusIndicator size={14} mini hideMessage /> - } - busy={hasClickedCreatePr} - analyticsEventName="Autofix: Create PR Clicked" - analyticsEventKey="autofix.create_pr_clicked" - analyticsParams={{group_id: groupId}} - > - Create PR{changes.length > 1 ? 's' : ''} - </Button> - ); -} - -function SetupAndCreatePRsButton({ - changes, - groupId, -}: { - changes: AutofixCodebaseChange[]; - groupId: string; -}) { - const {data: setupData} = useAutofixSetup({groupId, checkWriteAccess: true}); - - if ( - !changes.every( - change => - setupData?.githubWriteIntegration?.repos?.find( - repo => `${repo.owner}/${repo.name}` === change.repo_name - )?.ok - ) - ) { - return ( - <Button - priority="primary" - onClick={() => { - openModal(deps => <AutofixSetupWriteAccessModal {...deps} groupId={groupId} />); - }} - analyticsEventName="Autofix: Create PR Setup Clicked" - analyticsEventKey="autofix.create_pr_setup_clicked" - analyticsParams={{group_id: groupId}} - title={t('Enable write access to create pull requests')} - > - {t('Create PRs')} - </Button> - ); - } - - return <CreatePRsButton changes={changes} groupId={groupId} />; -} - interface RootCauseAndFeedbackInputAreaProps { actionText: string; changesMode: 'give_feedback' | 'add_tests' | 'create_prs' | null; @@ -275,10 +173,10 @@ function RootCauseAndFeedbackInputArea({ function StepIcon({step}: {step: AutofixStep}) { if (step.type === AutofixStepType.CHANGES) { - if (step.changes?.length === 0) { + if (Object.keys(step.codebase_changes).length === 0) { return <IconSad size="sm" color="gray300" />; } - if (step.changes.every(change => change.pull_request)) { + if (Object.values(step.codebase_changes).every(change => change.pull_request)) { return <IconCheckmark size="sm" color="green300" isCircled />; } return null; @@ -343,7 +241,9 @@ function AutofixMessageBox({ >(null); const changes = - isChangesStep && step?.type === AutofixStepType.CHANGES ? step.changes : []; + isChangesStep && step?.type === AutofixStepType.CHANGES + ? Object.values(step.codebase_changes) + : []; const prsMade = step?.status === AutofixStatus.COMPLETED && changes.length >= 1 && @@ -509,9 +409,13 @@ function AutofixMessageBox({ <InputArea> <StaticMessage> Draft {changes.length} pull request - {changes.length > 1 ? 's' : ''} for the above changes? + {changes.length > 1 ? 's' : ''} for the latest changes? </StaticMessage> - <SetupAndCreatePRsButton changes={changes} groupId={groupId} /> + <SetupAndCreatePRsButton + changes={changes} + groupId={groupId} + changesStepId={step!.id} + /> </InputArea> )} </Fragment> @@ -564,7 +468,7 @@ const Placeholder = styled('div')` const ViewPRButtons = styled(ScrollCarousel)` width: 100%; - padding: 0 ${space(1)}; + padding: 0 ${space(1)} ${space(2)} ${space(1)}; `; const ScrollIntoViewButtonWrapper = styled('div')` diff --git a/static/app/components/events/autofix/autofixPrButton.tsx b/static/app/components/events/autofix/autofixPrButton.tsx new file mode 100644 index 00000000000000..f3236b4144e977 --- /dev/null +++ b/static/app/components/events/autofix/autofixPrButton.tsx @@ -0,0 +1,140 @@ +import {useState} from 'react'; +import styled from '@emotion/styled'; + +import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; +import {openModal} from 'sentry/actionCreators/modal'; +import {Button} from 'sentry/components/button'; +import {AutofixSetupWriteAccessModal} from 'sentry/components/events/autofix/autofixSetupWriteAccessModal'; +import type {AutofixCodebaseChange} from 'sentry/components/events/autofix/types'; +import { + makeAutofixQueryKey, + useAutofixData, +} from 'sentry/components/events/autofix/useAutofix'; +import {useAutofixSetup} from 'sentry/components/events/autofix/useAutofixSetup'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; +import {t} from 'sentry/locale'; +import {useMutation, useQueryClient} from 'sentry/utils/queryClient'; +import useApi from 'sentry/utils/useApi'; + +function CreatePRsButton({ + changes, + groupId, + changesStepId, + isPrimary = true, +}: { + changes: AutofixCodebaseChange[]; + changesStepId: string; + groupId: string; + isPrimary?: boolean; +}) { + const autofixData = useAutofixData({groupId}); + const api = useApi(); + const queryClient = useQueryClient(); + const [hasClickedCreatePr, setHasClickedCreatePr] = useState(false); + + const createPRs = () => { + setHasClickedCreatePr(true); + for (const change of changes) { + createPr({change}); + } + }; + + const {mutate: createPr} = useMutation({ + mutationFn: ({change}: {change: AutofixCodebaseChange}) => { + return api.requestPromise(`/issues/${groupId}/autofix/update/`, { + method: 'POST', + data: { + run_id: autofixData?.run_id, + payload: { + type: 'create_pr', + repo_external_id: change.repo_external_id, + changes_step_id: changesStepId, + }, + }, + }); + }, + onSuccess: () => { + addSuccessMessage(t('Created pull requests.')); + queryClient.invalidateQueries({queryKey: makeAutofixQueryKey(groupId)}); + }, + onError: () => { + setHasClickedCreatePr(false); + addErrorMessage(t('Failed to create a pull request')); + }, + }); + + return ( + <Button + priority={isPrimary ? 'primary' : 'default'} + onClick={createPRs} + icon={ + hasClickedCreatePr && <ProcessingStatusIndicator size={14} mini hideMessage /> + } + busy={hasClickedCreatePr} + analyticsEventName="Autofix: Create PR Clicked" + analyticsEventKey="autofix.create_pr_clicked" + analyticsParams={{group_id: groupId}} + size="sm" + > + {changes.length > 1 ? t('Create PRs') : t('Create a PR')} + </Button> + ); +} + +export function SetupAndCreatePRsButton({ + changes, + groupId, + changesStepId, + hasStepBelow, + isPrimary = true, +}: { + changes: AutofixCodebaseChange[]; + changesStepId: string; + groupId: string; + hasStepBelow?: boolean; + isPrimary?: boolean; +}) { + const {data: setupData} = useAutofixSetup({groupId, checkWriteAccess: true}); + + const areAllReposWriteable = changes.every( + change => + setupData?.githubWriteIntegration?.repos?.find( + repo => `${repo.owner}/${repo.name}` === change.repo_name + )?.ok + ); + + if (!areAllReposWriteable) { + return ( + <Button + priority={hasStepBelow ? 'default' : 'primary'} + onClick={() => { + openModal(deps => <AutofixSetupWriteAccessModal {...deps} groupId={groupId} />); + }} + analyticsEventName="Autofix: Create PR Setup Clicked" + analyticsEventKey="autofix.create_pr_setup_clicked" + analyticsParams={{group_id: groupId}} + title={t('Enable write access to create pull requests')} + size="sm" + > + {changes.length > 1 ? t('Create PRs') : t('Create a PR')} + </Button> + ); + } + + return ( + <CreatePRsButton + changes={changes} + groupId={groupId} + changesStepId={changesStepId} + isPrimary={isPrimary} + /> + ); +} + +const ProcessingStatusIndicator = styled(LoadingIndicator)` + && { + margin: 0; + height: 14px; + width: 14px; + } +`; diff --git a/static/app/components/events/autofix/autofixSteps.tsx b/static/app/components/events/autofix/autofixSteps.tsx index d6e7d10b3b389f..2dd2ddf947276e 100644 --- a/static/app/components/events/autofix/autofixSteps.tsx +++ b/static/app/components/events/autofix/autofixSteps.tsx @@ -4,7 +4,7 @@ import {AnimatePresence, type AnimationProps, motion} from 'framer-motion'; import {AutofixChanges} from 'sentry/components/events/autofix/autofixChanges'; import AutofixInsightCards, { - useUpdateInsightCard, + useSendFeedbackOnChanges, } from 'sentry/components/events/autofix/autofixInsightCards'; import AutofixMessageBox from 'sentry/components/events/autofix/autofixMessageBox'; import {AutofixOutputStream} from 'sentry/components/events/autofix/autofixOutputStream'; @@ -16,6 +16,7 @@ import { type AutofixData, type AutofixProgressItem, type AutofixRepository, + AutofixStatus, type AutofixStep, AutofixStepType, } from 'sentry/components/events/autofix/types'; @@ -29,8 +30,10 @@ const animationProps: AnimationProps = { transition: testableTransition({duration: 0.3}), }; interface StepProps { + changesVersionIndex: number; groupId: string; hasErroredStepBefore: boolean; + hasMoreThanOneChangesStep: boolean; hasStepAbove: boolean; hasStepBelow: boolean; repos: AutofixRepository[]; @@ -70,6 +73,8 @@ export function Step({ hasStepAbove, hasErroredStepBefore, shouldHighlightRethink, + changesVersionIndex, + hasMoreThanOneChangesStep, }: StepProps) { return ( <StepCard> @@ -107,7 +112,14 @@ export function Step({ /> )} {step.type === AutofixStepType.CHANGES && ( - <AutofixChanges step={step} groupId={groupId} runId={runId} /> + <AutofixChanges + step={step} + groupId={groupId} + runId={runId} + hasStepBelow={hasStepBelow} + changesVersionIndex={changesVersionIndex} + hasMoreThanOneChangesStep={hasMoreThanOneChangesStep} + /> )} </Fragment> </AnimationWrapper> @@ -146,15 +158,9 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { } }; - const {mutate: sendFeedbackOnChanges} = useUpdateInsightCard({groupId, runId}); + const {mutate: sendFeedbackOnChanges} = useSendFeedbackOnChanges({groupId, runId}); const iterateOnChangesStep = (text: string) => { - const planStep = steps?.[steps.length - 2]; - if (!planStep || planStep.type !== AutofixStepType.DEFAULT) { - return; - } sendFeedbackOnChanges({ - step_index: planStep.index, - retain_insight_card_index: planStep.insights.length - 1, message: text, }); }; @@ -223,15 +229,21 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { const isRootCauseSelectionStep = lastStep.type === AutofixStepType.ROOT_CAUSE_ANALYSIS && - lastStep.status === 'COMPLETED'; + lastStep.status === AutofixStatus.COMPLETED; const isChangesStep = - lastStep.type === AutofixStepType.CHANGES && lastStep.status === 'COMPLETED'; + lastStep.type === AutofixStepType.CHANGES && + lastStep.status === AutofixStatus.COMPLETED; + + const changesStepIds = steps + .filter(step => step.type === AutofixStepType.CHANGES) + .map(step => step.id); return ( <div> <StepsContainer ref={containerRef}> {steps.map((step, index) => { + // TODO: Refactor and clean this up, very hard to follow const previousStep = index > 0 ? steps[index - 1] : null; const previousStepErrored = previousStep !== null && @@ -254,12 +266,19 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { nextStep?.insights?.length === 0; const isNextStepLastStep = index === steps.length - 2; + + // The current index of the changes step within all changes steps, or -1 if not a changes step + const changesVersionIndex = changesStepIds.indexOf(step.id); + const shouldHighlightRethink = (nextStep?.type === AutofixStepType.ROOT_CAUSE_ANALYSIS && isNextStepLastStep) || (nextStep?.type === AutofixStepType.CHANGES && - nextStep.changes.length > 0 && - !nextStep.changes.every(change => change.pull_request)); + Object.keys(nextStep.codebase_changes).length > 0 && + !Object.values(nextStep.codebase_changes).every( + change => change.pull_request + ) && + isNextStepLastStep); return ( <div ref={el => (stepsRef.current[index] = el)} key={step.id}> @@ -277,6 +296,8 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { repos={repos} hasErroredStepBefore={previousStepErrored} shouldHighlightRethink={shouldHighlightRethink} + changesVersionIndex={changesVersionIndex} + hasMoreThanOneChangesStep={changesStepIds.length > 1} /> </div> ); @@ -285,11 +306,10 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { <AutofixOutputStream stream={lastStep.output_stream} /> )} </StepsContainer> - <AutofixMessageBox displayText={activeLog ?? ''} step={lastStep} - responseRequired={lastStep.status === 'WAITING_FOR_USER_RESPONSE'} + responseRequired={lastStep.status === AutofixStatus.WAITING_FOR_USER_RESPONSE} onSend={ !isRootCauseSelectionStep ? !isChangesStep diff --git a/static/app/components/events/autofix/autofixViewPrButton.tsx b/static/app/components/events/autofix/autofixViewPrButton.tsx new file mode 100644 index 00000000000000..7c50fcc3886665 --- /dev/null +++ b/static/app/components/events/autofix/autofixViewPrButton.tsx @@ -0,0 +1,24 @@ +import {LinkButton} from 'sentry/components/button'; +import {IconOpen} from 'sentry/icons'; + +export function AutofixViewPrButton({ + repoName, + prUrl, + isPrimary = true, +}: { + prUrl: string; + repoName: string; + isPrimary?: boolean; +}) { + return ( + <LinkButton + size="xs" + priority={isPrimary ? 'primary' : 'default'} + icon={<IconOpen size="xs" />} + href={prUrl} + external + > + View PR in {repoName} + </LinkButton> + ); +} diff --git a/static/app/components/events/autofix/types.ts b/static/app/components/events/autofix/types.ts index 3426b284e04dec..dd48bc429cab5e 100644 --- a/static/app/components/events/autofix/types.ts +++ b/static/app/components/events/autofix/types.ts @@ -154,8 +154,8 @@ export type AutofixCodebaseChange = { repo_id?: number; // The repo_id is only here for temporary backwards compatibility for LA customers, and we should remove it soon. Use repo_external_id instead. }; -export interface AutofixChangesStep extends BaseStep { - changes: AutofixCodebaseChange[]; +export interface AutofixChangesStep extends Omit<AutofixDefaultStep, 'type'> { + codebase_changes: Record<string, AutofixCodebaseChange>; type: AutofixStepType.CHANGES; }