Skip to content

Commit 91676c0

Browse files
authored
chore(seer): Auto progress settings wizard on success (#95635)
When a step completes successfully, move on to the next step automatically. Also mark buttons as busy while a task is in progress
1 parent fd358e8 commit 91676c0

File tree

1 file changed

+148
-71
lines changed

1 file changed

+148
-71
lines changed

static/gsApp/views/seerAutomation/onboarding.tsx

Lines changed: 148 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {Flex} from 'sentry/components/core/layout';
1717
import {useOrganizationRepositories} from 'sentry/components/events/autofix/preferences/hooks/useOrganizationRepositories';
1818
import {useProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences';
1919
import {useUpdateProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useUpdateProjectSeerPreferences';
20-
import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps';
20+
import {
21+
GuidedSteps,
22+
useGuidedStepsContext,
23+
} from 'sentry/components/guidedSteps/guidedSteps';
2124
import ExternalLink from 'sentry/components/links/externalLink';
2225
import LoadingIndicator from 'sentry/components/loadingIndicator';
2326
import NoProjectMessage from 'sentry/components/noProjectMessage';
@@ -355,119 +358,198 @@ function ProjectsWithReposTracker({
355358
return null;
356359
}
357360

358-
function SeerAutomationOnboarding() {
361+
function AutoTriggerFixesButton({
362+
projectsWithRepos,
363+
selectedThreshold,
364+
fetching,
365+
}: {
366+
fetching: boolean;
367+
projectsWithRepos: Project[];
368+
selectedThreshold: string;
369+
}) {
359370
const organization = useOrganization();
360-
const {projects, fetching} = useProjects();
361371
const api = useApi();
362372
const queryClient = useQueryClient();
363-
const navigate = useNavigate();
364-
const [selectedThreshold, setSelectedThreshold] = useState('low');
365-
const [projectsWithRepos, setProjectsWithRepos] = useState<Project[]>([]);
366-
const [projectStates, setProjectStates] = useState<ProjectStateMap>({});
367-
const [successfullyConnectedProjects, setSuccessfullyConnectedProjects] = useState(
368-
new Set<string>()
369-
);
373+
const {setCurrentStep, getStepNumber} = useGuidedStepsContext();
374+
const [isLoading, setIsLoading] = useState(false);
370375

371-
const filteredProjects = useMemo(() => {
372-
return filterProjectsWithAccess(projects, organization);
373-
}, [projects, organization]);
374-
375-
const projectsWithoutRepos = useMemo(() => {
376-
return filteredProjects.filter(project => {
377-
// Exclude projects that have been successfully connected this session
378-
if (successfullyConnectedProjects.has(project.id)) return false;
379-
380-
// Exclude projects that already have repositories
381-
const state = projectStates[project.id];
382-
if (state && !state.isPending) {
383-
const repoCount = state.preference?.repositories?.length || 0;
384-
return repoCount === 0;
385-
}
386-
387-
// Include projects that are still loading (we don't know their repo status yet)
388-
return true;
389-
});
390-
}, [filteredProjects, successfullyConnectedProjects, projectStates]);
391-
392-
const handleEnableIssueScans = useCallback(async () => {
393-
if (projectsWithoutRepos.length === 0) {
394-
addErrorMessage(t('No remaining projects found to update'));
376+
const handleEnableAutoTriggerFixes = useCallback(async () => {
377+
if (projectsWithRepos.length === 0) {
378+
addErrorMessage(t('No projects with repositories found to update'));
395379
return;
396380
}
397381

398-
addLoadingMessage(t('Enabling issue scans for remaining projects...'), {
382+
addLoadingMessage(t('Enabling automation for projects with repositories...'), {
399383
duration: 30000,
400384
});
385+
setIsLoading(true);
401386

402387
try {
403388
await Promise.all(
404-
projectsWithoutRepos.map(project =>
389+
projectsWithRepos.map(project =>
405390
api.requestPromise(`/projects/${organization.slug}/${project.slug}/`, {
406391
method: 'PUT',
407-
data: {seerScannerAutomation: true},
392+
data: {
393+
autofixAutomationTuning: selectedThreshold,
394+
seerScannerAutomation: true,
395+
},
408396
})
409397
)
410398
);
411399

412400
addSuccessMessage(
413-
t('Issue scans enabled for %s remaining project(s)', projectsWithoutRepos.length)
401+
t(
402+
'Automation enabled for %s project(s) with repositories',
403+
projectsWithRepos.length
404+
)
414405
);
415406

416-
projectsWithoutRepos.forEach(project => {
407+
projectsWithRepos.forEach(project => {
417408
queryClient.invalidateQueries({
418409
queryKey: makeDetailedProjectQueryKey({
419410
orgSlug: organization.slug,
420411
projectSlug: project.slug,
421412
}),
422413
});
423414
});
415+
416+
// Automatically advance to the next step after successful completion
417+
const enableIssueScansStepNumber = getStepNumber('enable-issue-scans');
418+
setCurrentStep(enableIssueScansStepNumber);
424419
} catch (err) {
425-
addErrorMessage(t('Failed to enable issue scans for some projects'));
420+
addErrorMessage(t('Failed to enable automation for some projects'));
421+
} finally {
422+
setIsLoading(false);
426423
}
427-
}, [api, organization.slug, projectsWithoutRepos, queryClient]);
424+
}, [
425+
api,
426+
organization.slug,
427+
projectsWithRepos,
428+
selectedThreshold,
429+
queryClient,
430+
getStepNumber,
431+
setCurrentStep,
432+
]);
428433

429-
const handleEnableAutoTriggerFixes = useCallback(async () => {
430-
if (projectsWithRepos.length === 0) {
431-
addErrorMessage(t('No projects with repositories found to update'));
434+
return (
435+
<Button
436+
priority="primary"
437+
onClick={handleEnableAutoTriggerFixes}
438+
disabled={fetching || projectsWithRepos.length === 0 || isLoading}
439+
busy={isLoading}
440+
>
441+
{t('Enable for %s recommended project(s)', projectsWithRepos.length)}
442+
</Button>
443+
);
444+
}
445+
446+
function EnableIssueScansButton({
447+
projectsWithoutRepos,
448+
fetching,
449+
}: {
450+
fetching: boolean;
451+
projectsWithoutRepos: Project[];
452+
}) {
453+
const organization = useOrganization();
454+
const api = useApi();
455+
const queryClient = useQueryClient();
456+
const {setCurrentStep, getStepNumber} = useGuidedStepsContext();
457+
const [isLoading, setIsLoading] = useState(false);
458+
459+
const handleEnableIssueScans = useCallback(async () => {
460+
if (projectsWithoutRepos.length === 0) {
461+
addErrorMessage(t('No remaining projects found to update'));
432462
return;
433463
}
434464

435-
addLoadingMessage(t('Enabling automation for projects with repositories...'), {
465+
addLoadingMessage(t('Enabling issue scans for remaining projects...'), {
436466
duration: 30000,
437467
});
468+
setIsLoading(true);
438469

439470
try {
440471
await Promise.all(
441-
projectsWithRepos.map(project =>
472+
projectsWithoutRepos.map(project =>
442473
api.requestPromise(`/projects/${organization.slug}/${project.slug}/`, {
443474
method: 'PUT',
444-
data: {
445-
autofixAutomationTuning: selectedThreshold,
446-
seerScannerAutomation: true,
447-
},
475+
data: {seerScannerAutomation: true},
448476
})
449477
)
450478
);
451479

452480
addSuccessMessage(
453-
t(
454-
'Automation enabled for %s project(s) with repositories',
455-
projectsWithRepos.length
456-
)
481+
t('Issue scans enabled for %s remaining project(s)', projectsWithoutRepos.length)
457482
);
458483

459-
projectsWithRepos.forEach(project => {
484+
projectsWithoutRepos.forEach(project => {
460485
queryClient.invalidateQueries({
461486
queryKey: makeDetailedProjectQueryKey({
462487
orgSlug: organization.slug,
463488
projectSlug: project.slug,
464489
}),
465490
});
466491
});
492+
493+
// Automatically advance to the next step after successful completion
494+
const reviewCustomizeStepNumber = getStepNumber('review-customize');
495+
setCurrentStep(reviewCustomizeStepNumber);
467496
} catch (err) {
468-
addErrorMessage(t('Failed to enable automation for some projects'));
497+
addErrorMessage(t('Failed to enable issue scans for some projects'));
498+
} finally {
499+
setIsLoading(false);
469500
}
470-
}, [api, organization.slug, projectsWithRepos, selectedThreshold, queryClient]);
501+
}, [
502+
api,
503+
organization.slug,
504+
projectsWithoutRepos,
505+
queryClient,
506+
getStepNumber,
507+
setCurrentStep,
508+
]);
509+
510+
return (
511+
<Button
512+
priority="primary"
513+
onClick={handleEnableIssueScans}
514+
disabled={fetching || projectsWithoutRepos.length === 0 || isLoading}
515+
busy={isLoading}
516+
>
517+
{t('Enable for all projects')}
518+
</Button>
519+
);
520+
}
521+
522+
function SeerAutomationOnboarding() {
523+
const organization = useOrganization();
524+
const {projects, fetching} = useProjects();
525+
const navigate = useNavigate();
526+
const [selectedThreshold, setSelectedThreshold] = useState('low');
527+
const [projectsWithRepos, setProjectsWithRepos] = useState<Project[]>([]);
528+
const [projectStates, setProjectStates] = useState<ProjectStateMap>({});
529+
const [successfullyConnectedProjects, setSuccessfullyConnectedProjects] = useState(
530+
new Set<string>()
531+
);
532+
533+
const filteredProjects = useMemo(() => {
534+
return filterProjectsWithAccess(projects, organization);
535+
}, [projects, organization]);
536+
537+
const projectsWithoutRepos = useMemo(() => {
538+
return filteredProjects.filter(project => {
539+
// Exclude projects that have been successfully connected this session
540+
if (successfullyConnectedProjects.has(project.id)) return false;
541+
542+
// Exclude projects that already have repositories
543+
const state = projectStates[project.id];
544+
if (state && !state.isPending) {
545+
const repoCount = state.preference?.repositories?.length || 0;
546+
return repoCount === 0;
547+
}
548+
549+
// Include projects that are still loading (we don't know their repo status yet)
550+
return true;
551+
});
552+
}, [filteredProjects, successfullyConnectedProjects, projectStates]);
471553

472554
const handleProjectStatesUpdate = useCallback(
473555
(project: Project, preference: any, isPending: boolean) => {
@@ -561,13 +643,11 @@ function SeerAutomationOnboarding() {
561643
/>
562644
</Flex>
563645
</ThresholdSelectorWrapper>
564-
<Button
565-
priority="primary"
566-
onClick={handleEnableAutoTriggerFixes}
567-
disabled={fetching || projectsWithRepos.length === 0}
568-
>
569-
{t('Enable for %s recommended project(s)', projectsWithRepos.length)}
570-
</Button>
646+
<AutoTriggerFixesButton
647+
fetching={fetching}
648+
projectsWithRepos={projectsWithRepos}
649+
selectedThreshold={selectedThreshold}
650+
/>
571651
</Fragment>
572652
)}
573653

@@ -598,13 +678,10 @@ function SeerAutomationOnboarding() {
598678
</StepDescription>
599679

600680
<ScanActionWrapper>
601-
<Button
602-
priority="primary"
603-
onClick={handleEnableIssueScans}
604-
disabled={fetching || projectsWithoutRepos.length === 0}
605-
>
606-
{t('Enable for all projects')}
607-
</Button>
681+
<EnableIssueScansButton
682+
fetching={fetching}
683+
projectsWithoutRepos={projectsWithoutRepos}
684+
/>
608685
{projectsWithoutRepos.length === 0 && !fetching && (
609686
<EmptyProjectsMessage>
610687
{t('All projects are set up with Seer!')}

0 commit comments

Comments
 (0)