|
@@ -5,6 +5,7 @@ import {AnimatePresence, type AnimationProps, motion} from 'framer-motion';
|
|
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
|
|
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
|
|
import {openModal} from 'sentry/actionCreators/modal';
|
|
import {openModal} from 'sentry/actionCreators/modal';
|
|
import {Button, LinkButton} from 'sentry/components/button';
|
|
import {Button, LinkButton} from 'sentry/components/button';
|
|
|
|
+import ButtonBar from 'sentry/components/buttonBar';
|
|
import AutofixActionSelector from 'sentry/components/events/autofix/autofixActionSelector';
|
|
import AutofixActionSelector from 'sentry/components/events/autofix/autofixActionSelector';
|
|
import AutofixFeedback from 'sentry/components/events/autofix/autofixFeedback';
|
|
import AutofixFeedback from 'sentry/components/events/autofix/autofixFeedback';
|
|
import {AutofixSetupWriteAccessModal} from 'sentry/components/events/autofix/autofixSetupWriteAccessModal';
|
|
import {AutofixSetupWriteAccessModal} from 'sentry/components/events/autofix/autofixSetupWriteAccessModal';
|
|
@@ -27,6 +28,7 @@ import {
|
|
IconCheckmark,
|
|
IconCheckmark,
|
|
IconChevron,
|
|
IconChevron,
|
|
IconClose,
|
|
IconClose,
|
|
|
|
+ IconCopy,
|
|
IconFatal,
|
|
IconFatal,
|
|
IconOpen,
|
|
IconOpen,
|
|
IconSad,
|
|
IconSad,
|
|
@@ -37,6 +39,7 @@ import {singleLineRenderer} from 'sentry/utils/marked';
|
|
import {useMutation, useQueryClient} from 'sentry/utils/queryClient';
|
|
import {useMutation, useQueryClient} from 'sentry/utils/queryClient';
|
|
import testableTransition from 'sentry/utils/testableTransition';
|
|
import testableTransition from 'sentry/utils/testableTransition';
|
|
import useApi from 'sentry/utils/useApi';
|
|
import useApi from 'sentry/utils/useApi';
|
|
|
|
+import useCopyToClipboard from 'sentry/utils/useCopyToClipboard';
|
|
|
|
|
|
function useSendMessage({groupId, runId}: {groupId: string; runId: string}) {
|
|
function useSendMessage({groupId, runId}: {groupId: string; runId: string}) {
|
|
const api = useApi({persistInFlight: true});
|
|
const api = useApi({persistInFlight: true});
|
|
@@ -109,7 +112,6 @@ function CreatePRsButton({
|
|
payload: {
|
|
payload: {
|
|
type: 'create_pr',
|
|
type: 'create_pr',
|
|
repo_external_id: change.repo_external_id,
|
|
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.
|
|
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
@@ -136,7 +138,65 @@ function CreatePRsButton({
|
|
analyticsEventKey="autofix.create_pr_clicked"
|
|
analyticsEventKey="autofix.create_pr_clicked"
|
|
analyticsParams={{group_id: groupId}}
|
|
analyticsParams={{group_id: groupId}}
|
|
>
|
|
>
|
|
- Create PR{changes.length > 1 ? 's' : ''}
|
|
|
|
|
|
+ Draft PR{changes.length > 1 ? 's' : ''}
|
|
|
|
+ </Button>
|
|
|
|
+ );
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function CreateBranchButton({
|
|
|
|
+ changes,
|
|
|
|
+ groupId,
|
|
|
|
+}: {
|
|
|
|
+ changes: AutofixCodebaseChange[];
|
|
|
|
+ groupId: string;
|
|
|
|
+}) {
|
|
|
|
+ const autofixData = useAutofixData({groupId});
|
|
|
|
+ const api = useApi();
|
|
|
|
+ const queryClient = useQueryClient();
|
|
|
|
+ const [hasClickedPushToBranch, setHasClickedPushToBranch] = useState(false);
|
|
|
|
+
|
|
|
|
+ const pushToBranch = () => {
|
|
|
|
+ setHasClickedPushToBranch(true);
|
|
|
|
+ for (const change of changes) {
|
|
|
|
+ createBranch({change});
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const {mutate: createBranch} = useMutation({
|
|
|
|
+ mutationFn: ({change}: {change: AutofixCodebaseChange}) => {
|
|
|
|
+ return api.requestPromise(`/issues/${groupId}/autofix/update/`, {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ data: {
|
|
|
|
+ run_id: autofixData?.run_id,
|
|
|
|
+ payload: {
|
|
|
|
+ type: 'create_branch',
|
|
|
|
+ repo_external_id: change.repo_external_id,
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ });
|
|
|
|
+ },
|
|
|
|
+ onSuccess: () => {
|
|
|
|
+ addSuccessMessage(t('Pushed to branches.'));
|
|
|
|
+ queryClient.invalidateQueries({queryKey: makeAutofixQueryKey(groupId)});
|
|
|
|
+ },
|
|
|
|
+ onError: () => {
|
|
|
|
+ setHasClickedPushToBranch(false);
|
|
|
|
+ addErrorMessage(t('Failed to push to branches.'));
|
|
|
|
+ },
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <Button
|
|
|
|
+ onClick={pushToBranch}
|
|
|
|
+ icon={
|
|
|
|
+ hasClickedPushToBranch && <ProcessingStatusIndicator size={14} mini hideMessage />
|
|
|
|
+ }
|
|
|
|
+ busy={hasClickedPushToBranch}
|
|
|
|
+ analyticsEventName="Autofix: Push to Branch Clicked"
|
|
|
|
+ analyticsEventKey="autofix.push_to_branch_clicked"
|
|
|
|
+ analyticsParams={{group_id: groupId}}
|
|
|
|
+ >
|
|
|
|
+ Check Out Locally
|
|
</Button>
|
|
</Button>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
@@ -169,7 +229,7 @@ function SetupAndCreatePRsButton({
|
|
analyticsParams={{group_id: groupId}}
|
|
analyticsParams={{group_id: groupId}}
|
|
title={t('Enable write access to create pull requests')}
|
|
title={t('Enable write access to create pull requests')}
|
|
>
|
|
>
|
|
- {t('Create PRs')}
|
|
|
|
|
|
+ {t('Draft PR')}
|
|
</Button>
|
|
</Button>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
@@ -177,6 +237,41 @@ function SetupAndCreatePRsButton({
|
|
return <CreatePRsButton changes={changes} groupId={groupId} />;
|
|
return <CreatePRsButton changes={changes} groupId={groupId} />;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+function SetupAndCreateBranchButton({
|
|
|
|
+ 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
|
|
|
|
+ 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 branches')}
|
|
|
|
+ >
|
|
|
|
+ {t('Check Out Locally')}
|
|
|
|
+ </Button>
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return <CreateBranchButton changes={changes} groupId={groupId} />;
|
|
|
|
+}
|
|
|
|
+
|
|
interface RootCauseAndFeedbackInputAreaProps {
|
|
interface RootCauseAndFeedbackInputAreaProps {
|
|
actionText: string;
|
|
actionText: string;
|
|
changesMode: 'give_feedback' | 'add_tests' | 'create_prs' | null;
|
|
changesMode: 'give_feedback' | 'add_tests' | 'create_prs' | null;
|
|
@@ -348,6 +443,11 @@ function AutofixMessageBox({
|
|
step?.status === AutofixStatus.COMPLETED &&
|
|
step?.status === AutofixStatus.COMPLETED &&
|
|
changes.length >= 1 &&
|
|
changes.length >= 1 &&
|
|
changes.every(change => change.pull_request);
|
|
changes.every(change => change.pull_request);
|
|
|
|
+ const branchesMade =
|
|
|
|
+ !prsMade &&
|
|
|
|
+ step?.status === AutofixStatus.COMPLETED &&
|
|
|
|
+ changes.length >= 1 &&
|
|
|
|
+ changes.every(change => change.branch_name);
|
|
|
|
|
|
const isDisabled =
|
|
const isDisabled =
|
|
step?.status === AutofixStatus.ERROR ||
|
|
step?.status === AutofixStatus.ERROR ||
|
|
@@ -392,6 +492,27 @@ function AutofixMessageBox({
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ function BranchButton({change}: {change: AutofixCodebaseChange}) {
|
|
|
|
+ const {onClick} = useCopyToClipboard({
|
|
|
|
+ text: `git fetch --all && git switch ${change.branch_name}`,
|
|
|
|
+ successMessage: t('Command copied. Next stop: your terminal.'),
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <Button
|
|
|
|
+ key={`${change.repo_external_id}-${Math.random()}`}
|
|
|
|
+ size="xs"
|
|
|
|
+ priority="primary"
|
|
|
|
+ onClick={onClick}
|
|
|
|
+ aria-label={t('Check out in %s', change.repo_name)}
|
|
|
|
+ title={t('git fetch --all && git switch %s', change.branch_name)}
|
|
|
|
+ icon={<IconCopy size="xs" />}
|
|
|
|
+ >
|
|
|
|
+ {t('Check out in %s', change.repo_name)}
|
|
|
|
+ </Button>
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
return (
|
|
return (
|
|
<Container>
|
|
<Container>
|
|
<AnimatedContent animate={{height}} transition={{duration: 0.3, ease: 'easeInOut'}}>
|
|
<AnimatedContent animate={{height}} transition={{duration: 0.3, ease: 'easeInOut'}}>
|
|
@@ -466,12 +587,16 @@ function AutofixMessageBox({
|
|
/>
|
|
/>
|
|
)}
|
|
)}
|
|
</AutofixActionSelector>
|
|
</AutofixActionSelector>
|
|
- ) : isChangesStep && !prsMade ? (
|
|
|
|
|
|
+ ) : isChangesStep && !prsMade && !branchesMade ? (
|
|
<AutofixActionSelector
|
|
<AutofixActionSelector
|
|
options={[
|
|
options={[
|
|
{key: 'add_tests', label: t('Add tests')},
|
|
{key: 'add_tests', label: t('Add tests')},
|
|
{key: 'give_feedback', label: t('Iterate')},
|
|
{key: 'give_feedback', label: t('Iterate')},
|
|
- {key: 'create_prs', label: t('Approve'), active: true},
|
|
|
|
|
|
+ {
|
|
|
|
+ key: 'create_prs',
|
|
|
|
+ label: t('Use this code'),
|
|
|
|
+ active: true,
|
|
|
|
+ },
|
|
]}
|
|
]}
|
|
selected={changesMode}
|
|
selected={changesMode}
|
|
onSelect={value => setChangesMode(value)}
|
|
onSelect={value => setChangesMode(value)}
|
|
@@ -508,17 +633,29 @@ function AutofixMessageBox({
|
|
{option.key === 'create_prs' && (
|
|
{option.key === 'create_prs' && (
|
|
<InputArea>
|
|
<InputArea>
|
|
<StaticMessage>
|
|
<StaticMessage>
|
|
- Draft {changes.length} pull request
|
|
|
|
- {changes.length > 1 ? 's' : ''} for the above changes?
|
|
|
|
|
|
+ Push the above changes to{' '}
|
|
|
|
+ {changes.length > 1
|
|
|
|
+ ? `${changes.length} branches`
|
|
|
|
+ : 'a branch'}
|
|
|
|
+ ?
|
|
</StaticMessage>
|
|
</StaticMessage>
|
|
- <SetupAndCreatePRsButton changes={changes} groupId={groupId} />
|
|
|
|
|
|
+ <ButtonBar gap={1}>
|
|
|
|
+ <SetupAndCreateBranchButton
|
|
|
|
+ changes={changes}
|
|
|
|
+ groupId={groupId}
|
|
|
|
+ />
|
|
|
|
+ <SetupAndCreatePRsButton
|
|
|
|
+ changes={changes}
|
|
|
|
+ groupId={groupId}
|
|
|
|
+ />
|
|
|
|
+ </ButtonBar>
|
|
</InputArea>
|
|
</InputArea>
|
|
)}
|
|
)}
|
|
</Fragment>
|
|
</Fragment>
|
|
)}
|
|
)}
|
|
</AutofixActionSelector>
|
|
</AutofixActionSelector>
|
|
) : isChangesStep && prsMade ? (
|
|
) : isChangesStep && prsMade ? (
|
|
- <ViewPRButtons aria-label={t('View pull requests')}>
|
|
|
|
|
|
+ <StyledScrollCarousel aria-label={t('View pull requests')}>
|
|
{changes.map(
|
|
{changes.map(
|
|
change =>
|
|
change =>
|
|
change.pull_request?.pr_url && (
|
|
change.pull_request?.pr_url && (
|
|
@@ -534,7 +671,19 @@ function AutofixMessageBox({
|
|
</LinkButton>
|
|
</LinkButton>
|
|
)
|
|
)
|
|
)}
|
|
)}
|
|
- </ViewPRButtons>
|
|
|
|
|
|
+ </StyledScrollCarousel>
|
|
|
|
+ ) : isChangesStep && branchesMade ? (
|
|
|
|
+ <StyledScrollCarousel aria-label={t('Check out branches')}>
|
|
|
|
+ {changes.map(
|
|
|
|
+ change =>
|
|
|
|
+ change.branch_name && (
|
|
|
|
+ <BranchButton
|
|
|
|
+ key={`${change.repo_external_id}-${Math.random()}`}
|
|
|
|
+ change={change}
|
|
|
|
+ />
|
|
|
|
+ )
|
|
|
|
+ )}
|
|
|
|
+ </StyledScrollCarousel>
|
|
) : (
|
|
) : (
|
|
<RootCauseAndFeedbackInputArea
|
|
<RootCauseAndFeedbackInputArea
|
|
handleSend={handleSend}
|
|
handleSend={handleSend}
|
|
@@ -562,11 +711,6 @@ const Placeholder = styled('div')`
|
|
padding: ${space(1)};
|
|
padding: ${space(1)};
|
|
`;
|
|
`;
|
|
|
|
|
|
-const ViewPRButtons = styled(ScrollCarousel)`
|
|
|
|
- width: 100%;
|
|
|
|
- padding: 0 ${space(1)};
|
|
|
|
-`;
|
|
|
|
-
|
|
|
|
const ScrollIntoViewButtonWrapper = styled('div')`
|
|
const ScrollIntoViewButtonWrapper = styled('div')`
|
|
position: absolute;
|
|
position: absolute;
|
|
top: -2rem;
|
|
top: -2rem;
|
|
@@ -587,6 +731,10 @@ const Container = styled('div')`
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
+const StyledScrollCarousel = styled(ScrollCarousel)`
|
|
|
|
+ padding: 0 ${space(1)};
|
|
|
|
+`;
|
|
|
|
+
|
|
const AnimatedContent = styled(motion.div)`
|
|
const AnimatedContent = styled(motion.div)`
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
`;
|
|
`;
|