@@ -17,7 +17,10 @@ import {Flex} from 'sentry/components/core/layout';
17
17
import { useOrganizationRepositories } from 'sentry/components/events/autofix/preferences/hooks/useOrganizationRepositories' ;
18
18
import { useProjectSeerPreferences } from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences' ;
19
19
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' ;
21
24
import ExternalLink from 'sentry/components/links/externalLink' ;
22
25
import LoadingIndicator from 'sentry/components/loadingIndicator' ;
23
26
import NoProjectMessage from 'sentry/components/noProjectMessage' ;
@@ -355,119 +358,198 @@ function ProjectsWithReposTracker({
355
358
return null ;
356
359
}
357
360
358
- function SeerAutomationOnboarding ( ) {
361
+ function AutoTriggerFixesButton ( {
362
+ projectsWithRepos,
363
+ selectedThreshold,
364
+ fetching,
365
+ } : {
366
+ fetching : boolean ;
367
+ projectsWithRepos : Project [ ] ;
368
+ selectedThreshold : string ;
369
+ } ) {
359
370
const organization = useOrganization ( ) ;
360
- const { projects, fetching} = useProjects ( ) ;
361
371
const api = useApi ( ) ;
362
372
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 ) ;
370
375
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' ) ) ;
395
379
return ;
396
380
}
397
381
398
- addLoadingMessage ( t ( 'Enabling issue scans for remaining projects...' ) , {
382
+ addLoadingMessage ( t ( 'Enabling automation for projects with repositories ...' ) , {
399
383
duration : 30000 ,
400
384
} ) ;
385
+ setIsLoading ( true ) ;
401
386
402
387
try {
403
388
await Promise . all (
404
- projectsWithoutRepos . map ( project =>
389
+ projectsWithRepos . map ( project =>
405
390
api . requestPromise ( `/projects/${ organization . slug } /${ project . slug } /` , {
406
391
method : 'PUT' ,
407
- data : { seerScannerAutomation : true } ,
392
+ data : {
393
+ autofixAutomationTuning : selectedThreshold ,
394
+ seerScannerAutomation : true ,
395
+ } ,
408
396
} )
409
397
)
410
398
) ;
411
399
412
400
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
+ )
414
405
) ;
415
406
416
- projectsWithoutRepos . forEach ( project => {
407
+ projectsWithRepos . forEach ( project => {
417
408
queryClient . invalidateQueries ( {
418
409
queryKey : makeDetailedProjectQueryKey ( {
419
410
orgSlug : organization . slug ,
420
411
projectSlug : project . slug ,
421
412
} ) ,
422
413
} ) ;
423
414
} ) ;
415
+
416
+ // Automatically advance to the next step after successful completion
417
+ const enableIssueScansStepNumber = getStepNumber ( 'enable-issue-scans' ) ;
418
+ setCurrentStep ( enableIssueScansStepNumber ) ;
424
419
} 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 ) ;
426
423
}
427
- } , [ api , organization . slug , projectsWithoutRepos , queryClient ] ) ;
424
+ } , [
425
+ api ,
426
+ organization . slug ,
427
+ projectsWithRepos ,
428
+ selectedThreshold ,
429
+ queryClient ,
430
+ getStepNumber ,
431
+ setCurrentStep ,
432
+ ] ) ;
428
433
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' ) ) ;
432
462
return ;
433
463
}
434
464
435
- addLoadingMessage ( t ( 'Enabling automation for projects with repositories ...' ) , {
465
+ addLoadingMessage ( t ( 'Enabling issue scans for remaining projects ...' ) , {
436
466
duration : 30000 ,
437
467
} ) ;
468
+ setIsLoading ( true ) ;
438
469
439
470
try {
440
471
await Promise . all (
441
- projectsWithRepos . map ( project =>
472
+ projectsWithoutRepos . map ( project =>
442
473
api . requestPromise ( `/projects/${ organization . slug } /${ project . slug } /` , {
443
474
method : 'PUT' ,
444
- data : {
445
- autofixAutomationTuning : selectedThreshold ,
446
- seerScannerAutomation : true ,
447
- } ,
475
+ data : { seerScannerAutomation : true } ,
448
476
} )
449
477
)
450
478
) ;
451
479
452
480
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 )
457
482
) ;
458
483
459
- projectsWithRepos . forEach ( project => {
484
+ projectsWithoutRepos . forEach ( project => {
460
485
queryClient . invalidateQueries ( {
461
486
queryKey : makeDetailedProjectQueryKey ( {
462
487
orgSlug : organization . slug ,
463
488
projectSlug : project . slug ,
464
489
} ) ,
465
490
} ) ;
466
491
} ) ;
492
+
493
+ // Automatically advance to the next step after successful completion
494
+ const reviewCustomizeStepNumber = getStepNumber ( 'review-customize' ) ;
495
+ setCurrentStep ( reviewCustomizeStepNumber ) ;
467
496
} 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 ) ;
469
500
}
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 ] ) ;
471
553
472
554
const handleProjectStatesUpdate = useCallback (
473
555
( project : Project , preference : any , isPending : boolean ) => {
@@ -561,13 +643,11 @@ function SeerAutomationOnboarding() {
561
643
/>
562
644
</ Flex >
563
645
</ 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
+ />
571
651
</ Fragment >
572
652
) }
573
653
@@ -598,13 +678,10 @@ function SeerAutomationOnboarding() {
598
678
</ StepDescription >
599
679
600
680
< 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
+ />
608
685
{ projectsWithoutRepos . length === 0 && ! fetching && (
609
686
< EmptyProjectsMessage >
610
687
{ t ( 'All projects are set up with Seer!' ) }
0 commit comments