From d9a3abec64e8a099821f53d6ccf0ff8222731f40 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Wed, 25 Mar 2026 16:58:03 -0700 Subject: [PATCH 1/8] feat(seer): Seer Code Review Overview section --- .../useBulkUpdateRepositorySettings.tsx | 3 +- .../useRepositoryWithSettings.tsx | 0 .../overview/codeReviewOverviewSection.tsx | 311 ++++++++++++++++++ .../overview/useSeerOverviewData.spec.tsx | 269 +++++++++++++++ .../seer/overview/useSeerOverviewData.tsx | 106 ++++++ .../repoDetails/repoDetailsForm.tsx | 2 +- .../components/repoTable/seerRepoTable.tsx | 4 +- .../repoTable/seerRepoTableHeader.tsx | 2 +- .../components/repoTable/seerRepoTableRow.tsx | 10 +- .../onboarding/configureCodeReviewStep.tsx | 2 +- .../views/seerAutomation/repoDetails.tsx | 2 +- .../gsApp/views/seerAutomation/settings.tsx | 152 +++++---- 12 files changed, 782 insertions(+), 81 deletions(-) rename static/{gsApp/views/seerAutomation/onboarding/hooks => app/components/repositories}/useBulkUpdateRepositorySettings.tsx (92%) rename static/{gsApp/views/seerAutomation/onboarding/hooks => app/components/repositories}/useRepositoryWithSettings.tsx (100%) create mode 100644 static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx create mode 100644 static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx create mode 100644 static/app/views/settings/seer/overview/useSeerOverviewData.tsx diff --git a/static/gsApp/views/seerAutomation/onboarding/hooks/useBulkUpdateRepositorySettings.tsx b/static/app/components/repositories/useBulkUpdateRepositorySettings.tsx similarity index 92% rename from static/gsApp/views/seerAutomation/onboarding/hooks/useBulkUpdateRepositorySettings.tsx rename to static/app/components/repositories/useBulkUpdateRepositorySettings.tsx index cd704672510560..114813e0f6c2dc 100644 --- a/static/gsApp/views/seerAutomation/onboarding/hooks/useBulkUpdateRepositorySettings.tsx +++ b/static/app/components/repositories/useBulkUpdateRepositorySettings.tsx @@ -1,3 +1,4 @@ +import {getRepositoryWithSettingsQueryKey} from 'sentry/components/repositories/useRepositoryWithSettings'; import type {RepositoryWithSettings} from 'sentry/types/integrations'; import { fetchMutation, @@ -7,8 +8,6 @@ import { } from 'sentry/utils/queryClient'; import {useOrganization} from 'sentry/utils/useOrganization'; -import {getRepositoryWithSettingsQueryKey} from 'getsentry/views/seerAutomation/onboarding/hooks/useRepositoryWithSettings'; - export type RepositorySettings = | { enabledCodeReview: boolean; diff --git a/static/gsApp/views/seerAutomation/onboarding/hooks/useRepositoryWithSettings.tsx b/static/app/components/repositories/useRepositoryWithSettings.tsx similarity index 100% rename from static/gsApp/views/seerAutomation/onboarding/hooks/useRepositoryWithSettings.tsx rename to static/app/components/repositories/useRepositoryWithSettings.tsx diff --git a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx new file mode 100644 index 00000000000000..d9eae1ad4daf37 --- /dev/null +++ b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx @@ -0,0 +1,311 @@ +import {useCallback} from 'react'; +import {mutationOptions} from '@tanstack/react-query'; +import uniqBy from 'lodash/uniqBy'; +import {z} from 'zod'; + +import {Button} from '@sentry/scraps/button'; +import {AutoSaveForm, FieldGroup} from '@sentry/scraps/form'; +import {Container, Flex, Stack} from '@sentry/scraps/layout'; +import {Link} from '@sentry/scraps/link'; +import {Text} from '@sentry/scraps/text'; + +import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; +import {updateOrganization} from 'sentry/actionCreators/organizations'; +import {hasEveryAccess} from 'sentry/components/acl/access'; +import {organizationRepositoriesInfiniteOptions} from 'sentry/components/events/autofix/preferences/hooks/useOrganizationRepositories'; +import {isSupportedAutofixProvider} from 'sentry/components/events/autofix/utils'; +import {useBulkUpdateRepositorySettings} from 'sentry/components/repositories/useBulkUpdateRepositorySettings'; +import {getRepositoryWithSettingsQueryKey} from 'sentry/components/repositories/useRepositoryWithSettings'; +import {IconRefresh, IconSettings} from 'sentry/icons'; +import {t, tct, tn} from 'sentry/locale'; +import {DEFAULT_CODE_REVIEW_TRIGGERS} from 'sentry/types/integrations'; +import type {Organization} from 'sentry/types/organization'; +import {useFetchAllPages} from 'sentry/utils/api/apiFetch'; +import {useInfiniteQuery, useQueryClient} from 'sentry/utils/queryClient'; +import {fetchMutation} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; + +export function useCodeReviewOverviewSection() { + const organization = useOrganization(); + + const queryOptions = organizationRepositoriesInfiniteOptions({ + organization, + query: {per_page: 100}, + }); + const repositoriesResult = useInfiniteQuery({ + ...queryOptions, + select: ({pages}) => { + const repos = uniqBy( + pages.flatMap(page => page.json), + 'externalId' + ).filter(repository => repository.externalId); + const seerRepos = repos.filter(r => isSupportedAutofixProvider(r.provider)); + const reposWithCodeReview = seerRepos.filter(r => r.settings?.enabledCodeReview); + return { + queryKey: queryOptions.queryKey, + seerRepos, + reposWithCodeReview, + }; + }, + }); + useFetchAllPages({result: repositoriesResult}); + + return repositoriesResult; +} + +type Props = ReturnType & { + canWrite: boolean; + organization: Organization; +}; + +export function CodeReviewOverviewSection({ + isPending, + organization, + data, + refetch, +}: Props) { + const queryClient = useQueryClient(); + + const canWrite = hasEveryAccess(['org:write'], {organization}); + + const {queryKey, seerRepos = [], reposWithCodeReview = []} = data ?? {}; + + const seerReposCount = seerRepos.length; + const reposWithCodeReviewCount = reposWithCodeReview.length; + + const schema = z.object({ + autoEnableCodeReview: z.boolean(), + defaultCodeReviewTriggers: z.array(z.enum(['on_new_commit', 'on_ready_for_review'])), + }); + + const orgMutationOpts = mutationOptions({ + mutationFn: (updateData: Partial) => + fetchMutation({ + method: 'PUT', + url: `/organizations/${organization.slug}/`, + data: updateData, + }), + onSuccess: updateOrganization, + }); + + const {mutate: mutateRepositorySettings} = useBulkUpdateRepositorySettings({ + onSettled: mutations => { + // Invalidate the repositories query to get the updated settings + queryClient.invalidateQueries({queryKey}); + (mutations ?? []).forEach(mutation => { + // Invalidate related queries + queryClient.invalidateQueries({ + queryKey: getRepositoryWithSettingsQueryKey(organization, mutation.id), + }); + }); + }, + }); + + const handleToggleCodeReview = useCallback( + (enabledCodeReview: boolean) => { + const repositoryIds = ( + enabledCodeReview + ? seerRepos.filter(repo => !repo.settings?.enabledCodeReview) + : reposWithCodeReview + ).map(repo => repo.id); + mutateRepositorySettings( + {enabledCodeReview, repositoryIds}, + { + onError: (_, variables) => { + addErrorMessage( + tn( + 'Failed to update code review for %s repository', + 'Failed to update code review for %s repositories', + variables.repositoryIds.length + ) + ); + }, + onSuccess: (_, variables) => { + addSuccessMessage( + tn( + 'Code review updated for %s repository', + 'Code review updated for %s repositories', + variables.repositoryIds.length + ) + ); + }, + } + ); + }, + [mutateRepositorySettings, reposWithCodeReview, seerRepos] + ); + + const handleChangeTriggers = useCallback( + (newTriggers: string[]) => { + mutateRepositorySettings( + { + codeReviewTriggers: newTriggers, + repositoryIds: seerRepos.map(repo => repo.id), + }, + { + onError: (_, variables) => { + addErrorMessage( + tn( + 'Failed to update triggers for %s repository', + 'Failed to update triggers for %s repositories', + variables.repositoryIds.length + ) + ); + }, + onSuccess: (_, variables) => { + addSuccessMessage( + tn( + 'Triggers updated for %s repository', + 'Triggers updated for %s repositories', + variables.repositoryIds.length + ) + ); + }, + } + ); + }, + [mutateRepositorySettings, seerRepos] + ); + + return ( + + + {t('Code Review')} + + + {field.state.value + ? t( + '%s of %s existing repos have code review enabled', + reposWithCodeReviewCount, + seerReposCount + ) + : t( + '%s of %s existing repos have code review disabled', + seerReposCount - reposWithCodeReviewCount, + seerReposCount + )} + + + + )} + + + + {field => ( + + } + )} + > + + + + + + + + + )} + + + ); +} diff --git a/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx b/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx new file mode 100644 index 00000000000000..abfabb69dcd9d2 --- /dev/null +++ b/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx @@ -0,0 +1,269 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {RepositoryFixture} from 'sentry-fixture/repository'; + +import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary'; + +import type { + OrganizationIntegration, + RepositoryWithSettings, +} from 'sentry/types/integrations'; +import {useSeerOverviewData} from 'sentry/views/settings/seer/overview/useSeerOverviewData'; + +function RepoWithSettingsFixture( + params: Partial = {} +): RepositoryWithSettings { + return { + ...RepositoryFixture(), + settings: null, + ...params, + }; +} + +function IntegrationFixture( + params: Partial & {features?: string[]} = {} +): OrganizationIntegration { + const {features = ['commits'], ...rest} = params; + return { + id: 'integration-1', + name: 'Test Integration', + domainName: 'github.com/test', + icon: null, + accountType: null, + gracePeriodEnd: null, + organizationIntegrationStatus: 'active', + status: 'active', + externalId: 'ext-integration-1', + organizationId: '1', + configData: null, + configOrganization: [], + provider: { + key: 'github', + slug: 'github', + name: 'GitHub', + canAdd: true, + canDisable: false, + features, + aspects: {}, + }, + ...rest, + }; +} + +describe('useSeerOverviewData', () => { + const organization = OrganizationFixture({slug: 'org-slug'}); + + afterEach(() => { + MockApiClient.clearMockResponses(); + }); + + function setupMocks({ + repos = [], + autofixSettings = [], + integrations = [], + }: { + autofixSettings?: Array<{ + autofixAutomationTuning: string | null; + projectId: string; + reposCount: number; + }>; + integrations?: OrganizationIntegration[]; + repos?: RepositoryWithSettings[]; + } = {}) { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/integrations/', + method: 'GET', + body: integrations, + }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/repos/', + method: 'GET', + body: repos, + }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/autofix/automation-settings/', + method: 'GET', + body: autofixSettings, + }); + } + + it('returns zeroed stats when there are no repos or projects', async () => { + setupMocks(); + + const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.stats).toEqual({ + integrationCount: 0, + totalRepoCount: 0, + seerRepoCount: 0, + reposWithSettingsCount: 0, + projectsWithReposCount: 0, + projectsWithAutomationCount: 0, + totalProjects: 0, + reposWithCodeReviewCount: 0, + }); + }); + + it('counts repos and integrations with commits feature', async () => { + setupMocks({ + repos: [ + RepoWithSettingsFixture({ + id: '1', + externalId: 'ext-1', + integrationId: 'integration-a', + provider: {id: 'integrations:github', name: 'GitHub'}, + }), + RepoWithSettingsFixture({ + id: '2', + externalId: 'ext-2', + integrationId: 'integration-a', + provider: {id: 'integrations:github', name: 'GitHub'}, + }), + RepoWithSettingsFixture({ + id: '3', + externalId: 'ext-3', + integrationId: 'integration-b', + provider: {id: 'integrations:github', name: 'GitHub'}, + }), + ], + integrations: [ + IntegrationFixture({id: 'integration-a', features: ['commits', 'issue-basic']}), + IntegrationFixture({id: 'integration-b', features: ['commits']}), + IntegrationFixture({id: 'integration-c', features: ['issue-basic']}), // no commits + ], + }); + + const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.stats.totalRepoCount).toBe(3); + expect(result.current.stats.seerRepoCount).toBe(3); + expect(result.current.stats.integrationCount).toBe(2); // only integrations with 'commits' + }); + + it('only counts repos with supported providers toward seerRepoCount', async () => { + setupMocks({ + repos: [ + RepoWithSettingsFixture({ + id: '1', + externalId: 'ext-1', + integrationId: 'integration-a', + provider: {id: 'integrations:github', name: 'GitHub'}, + }), + RepoWithSettingsFixture({ + id: '2', + externalId: 'ext-2', + integrationId: 'integration-b', + provider: {id: 'integrations:bitbucket', name: 'Bitbucket'}, // unsupported + }), + ], + }); + + const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.stats.totalRepoCount).toBe(2); + expect(result.current.stats.seerRepoCount).toBe(1); + }); + + it('counts repos with code review enabled', async () => { + setupMocks({ + repos: [ + RepoWithSettingsFixture({ + id: '1', + externalId: 'ext-1', + integrationId: 'integration-a', + provider: {id: 'integrations:github', name: 'GitHub'}, + settings: {enabledCodeReview: true, codeReviewTriggers: []}, + }), + RepoWithSettingsFixture({ + id: '2', + externalId: 'ext-2', + integrationId: 'integration-a', + provider: {id: 'integrations:github', name: 'GitHub'}, + settings: {enabledCodeReview: false, codeReviewTriggers: []}, + }), + RepoWithSettingsFixture({ + id: '3', + externalId: 'ext-3', + integrationId: 'integration-a', + provider: {id: 'integrations:github', name: 'GitHub'}, + settings: null, // no settings + }), + ], + }); + + const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.stats.reposWithCodeReviewCount).toBe(1); + }); + + it('counts projects with repos and with automation enabled', async () => { + setupMocks({ + autofixSettings: [ + {projectId: '1', reposCount: 2, autofixAutomationTuning: 'medium'}, + {projectId: '2', reposCount: 1, autofixAutomationTuning: 'off'}, + {projectId: '3', reposCount: 0, autofixAutomationTuning: 'off'}, + {projectId: '4', reposCount: 0, autofixAutomationTuning: 'medium'}, + ], + }); + + const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.stats.totalProjects).toBe(4); + expect(result.current.stats.projectsWithReposCount).toBe(2); + expect(result.current.stats.projectsWithAutomationCount).toBe(2); + }); + + it('counts all non-off automation tuning values as enabled', async () => { + setupMocks({ + autofixSettings: [ + {projectId: '1', reposCount: 1, autofixAutomationTuning: 'medium'}, + {projectId: '2', reposCount: 1, autofixAutomationTuning: 'high'}, + {projectId: '3', reposCount: 1, autofixAutomationTuning: 'always'}, + {projectId: '4', reposCount: 0, autofixAutomationTuning: 'off'}, + {projectId: '5', reposCount: 0, autofixAutomationTuning: null}, + ], + }); + + const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + // only 'off' means disabled; null (deprecated) is also treated as enabled + expect(result.current.stats.projectsWithAutomationCount).toBe(4); + }); + + it('deduplicates repos by externalId', async () => { + setupMocks({ + repos: [ + RepoWithSettingsFixture({ + id: '1', + externalId: 'same-external-id', + integrationId: 'integration-a', + provider: {id: 'integrations:github', name: 'GitHub'}, + }), + RepoWithSettingsFixture({ + id: '2', + externalId: 'same-external-id', // duplicate + integrationId: 'integration-b', + provider: {id: 'integrations:github', name: 'GitHub'}, + }), + ], + }); + + const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.stats.totalRepoCount).toBe(1); + expect(result.current.stats.seerRepoCount).toBe(1); + }); +}); diff --git a/static/app/views/settings/seer/overview/useSeerOverviewData.tsx b/static/app/views/settings/seer/overview/useSeerOverviewData.tsx new file mode 100644 index 00000000000000..0770bc4c2229e3 --- /dev/null +++ b/static/app/views/settings/seer/overview/useSeerOverviewData.tsx @@ -0,0 +1,106 @@ +import {useMemo} from 'react'; +import uniqBy from 'lodash/uniqBy'; + +import {bulkAutofixAutomationSettingsInfiniteOptions} from 'sentry/components/events/autofix/preferences/hooks/useBulkAutofixAutomationSettings'; +import {organizationRepositoriesInfiniteOptions} from 'sentry/components/events/autofix/preferences/hooks/useOrganizationRepositories'; +import {isSupportedAutofixProvider} from 'sentry/components/events/autofix/utils'; +import {organizationIntegrationsQueryOptions} from 'sentry/endpoints/organizations/organizationsIntegrationsQueryOptions'; +import {useFetchAllPages} from 'sentry/utils/api/apiFetch'; +import {useInfiniteQuery, useQuery} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; + +export function useSeerOverviewData() { + const organization = useOrganization(); + + // SCM Data + const {data: integrationData, isPending: isIntegrationsPending} = useQuery({ + ...organizationIntegrationsQueryOptions({organization}), + select: data => { + const allIntegrations = data.json.filter(i => i !== null); + const scmIntegrations = allIntegrations.filter(integration => + integration.provider.features.includes('commits') + ); + const seerIntegrations = scmIntegrations.filter(integration => + isSupportedAutofixProvider({ + id: integration.provider.key, + name: integration.provider.name, + }) + ); + return { + integrations: allIntegrations, + scmIntegrations, + seerIntegrations, + }; + }, + }); + + // Repos Data + const repositoriesResult = useInfiniteQuery({ + ...organizationRepositoriesInfiniteOptions({ + organization, + query: {per_page: 100}, + }), + select: ({pages}) => { + const allRepos = uniqBy( + pages.flatMap(page => page.json), + 'externalId' + ).filter(repository => repository.externalId); + const seerRepos = allRepos.filter(r => isSupportedAutofixProvider(r.provider)); + return { + allRepos, + seerRepos, + reposWithSettings: seerRepos.filter(r => r.settings !== null), + reposWithCodeReview: seerRepos.filter(r => r.settings?.enabledCodeReview), + }; + }, + }); + useFetchAllPages({result: repositoriesResult}); + const {data: repositoryData, isPending: isReposPending} = repositoriesResult; + + // Autofix Data + const autofixSettingsResult = useInfiniteQuery({ + ...bulkAutofixAutomationSettingsInfiniteOptions({organization}), + select: ({pages}) => { + const autofixItems = pages.flatMap(page => page.json).filter(s => s !== null); + return { + autofixItems, + projectsWithRepos: autofixItems.filter(settings => settings.reposCount > 0), + projectsWithAutomation: autofixItems.filter( + settings => settings.autofixAutomationTuning !== 'off' + ), + projectsWithCreatePr: autofixItems.filter( + settings => settings.automationHandoff?.auto_create_pr + ), + }; + }, + }); + useFetchAllPages({result: autofixSettingsResult}); + const {data: autofixData, isPending: isAutofixPending} = autofixSettingsResult; + + const stats = useMemo(() => { + return { + // SCM Stats + integrationCount: integrationData?.integrations.length ?? 0, + scmIntegrationCount: integrationData?.scmIntegrations.length ?? 0, + seerIntegrations: integrationData?.seerIntegrations ?? [], + seerIntegrationCount: integrationData?.seerIntegrations.length ?? 0, + + // Autofix Stats + totalProjects: autofixData?.autofixItems.length ?? 0, + projectsWithReposCount: autofixData?.projectsWithRepos.length ?? 0, + projectsWithAutomationCount: autofixData?.projectsWithAutomation.length ?? 0, + projectsWithCreatePrCount: autofixData?.projectsWithCreatePr.length ?? 0, + + // Repos Stats + totalRepoCount: repositoryData?.allRepos.length ?? 0, + seerRepoCount: repositoryData?.seerRepos.length ?? 0, + reposWithSettingsCount: repositoryData?.reposWithSettings.length ?? 0, + reposWithCodeReviewCount: repositoryData?.reposWithCodeReview.length ?? 0, + }; + }, [integrationData, autofixData, repositoryData]); + + return { + stats, + isLoading: isIntegrationsPending || isReposPending || isAutofixPending, + }; +} diff --git a/static/gsApp/views/seerAutomation/components/repoDetails/repoDetailsForm.tsx b/static/gsApp/views/seerAutomation/components/repoDetails/repoDetailsForm.tsx index 36d787f8fba4e1..661b2a7301d171 100644 --- a/static/gsApp/views/seerAutomation/components/repoDetails/repoDetailsForm.tsx +++ b/static/gsApp/views/seerAutomation/components/repoDetails/repoDetailsForm.tsx @@ -3,12 +3,12 @@ import {Stack} from '@sentry/scraps/layout'; import {Form} from 'sentry/components/forms/form'; import JsonForm from 'sentry/components/forms/jsonForm'; +import {type RepositorySettings} from 'sentry/components/repositories/useBulkUpdateRepositorySettings'; import {t, tct} from 'sentry/locale'; import {type RepositoryWithSettings} from 'sentry/types/integrations'; import type {Organization} from 'sentry/types/organization'; import {useCanWriteSettings} from 'getsentry/views/seerAutomation/components/useCanWriteSettings'; -import {type RepositorySettings} from 'getsentry/views/seerAutomation/onboarding/hooks/useBulkUpdateRepositorySettings'; interface Props { organization: Organization; diff --git a/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTable.tsx b/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTable.tsx index efaff98ca34234..e3a8d500b7b70a 100644 --- a/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTable.tsx +++ b/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTable.tsx @@ -17,6 +17,8 @@ import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {Panel} from 'sentry/components/panels/panel'; import {ScmRepoTreeModal} from 'sentry/components/repositories/scmRepoTreeModal'; +import {useBulkUpdateRepositorySettings} from 'sentry/components/repositories/useBulkUpdateRepositorySettings'; +import {getRepositoryWithSettingsQueryKey} from 'sentry/components/repositories/useRepositoryWithSettings'; import {IconAdd} from 'sentry/icons'; import {IconSearch} from 'sentry/icons/iconSearch'; import {t, tct} from 'sentry/locale'; @@ -32,8 +34,6 @@ import {useOrganization} from 'sentry/utils/useOrganization'; import {SeerRepoTableHeader} from 'getsentry/views/seerAutomation/components/repoTable/seerRepoTableHeader'; import {SeerRepoTableRow} from 'getsentry/views/seerAutomation/components/repoTable/seerRepoTableRow'; -import {useBulkUpdateRepositorySettings} from 'getsentry/views/seerAutomation/onboarding/hooks/useBulkUpdateRepositorySettings'; -import {getRepositoryWithSettingsQueryKey} from 'getsentry/views/seerAutomation/onboarding/hooks/useRepositoryWithSettings'; const GRID_COLUMNS = '40px 1fr 118px 150px'; const SELECTED_ROW_HEIGHT = 44; diff --git a/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTableHeader.tsx b/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTableHeader.tsx index 54464c61b68585..2154026373c489 100644 --- a/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTableHeader.tsx +++ b/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTableHeader.tsx @@ -8,6 +8,7 @@ import {Flex} from '@sentry/scraps/layout'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import {QuestionTooltip} from 'sentry/components/questionTooltip'; +import type {useBulkUpdateRepositorySettings} from 'sentry/components/repositories/useBulkUpdateRepositorySettings'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {t, tct, tn} from 'sentry/locale'; import {parseQueryKey} from 'sentry/utils/api/apiQueryKey'; @@ -15,7 +16,6 @@ import type {Sort} from 'sentry/utils/discover/fields'; import {useListItemCheckboxContext} from 'sentry/utils/list/useListItemCheckboxState'; import {useCanWriteSettings} from 'getsentry/views/seerAutomation/components/useCanWriteSettings'; -import type {useBulkUpdateRepositorySettings} from 'getsentry/views/seerAutomation/onboarding/hooks/useBulkUpdateRepositorySettings'; interface Props { gridColumns: string; diff --git a/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTableRow.tsx b/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTableRow.tsx index ecca8455caf784..0208f6e6de1bd7 100644 --- a/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTableRow.tsx +++ b/static/gsApp/views/seerAutomation/components/repoTable/seerRepoTableRow.tsx @@ -12,6 +12,11 @@ import { addSuccessMessage, } from 'sentry/actionCreators/indicator'; import {getRepoStatusLabel} from 'sentry/components/repositories/getRepoStatusLabel'; +import {useBulkUpdateRepositorySettings} from 'sentry/components/repositories/useBulkUpdateRepositorySettings'; +import { + getRepositoryWithSettingsQueryKey, + useRepositoryWithSettings, +} from 'sentry/components/repositories/useRepositoryWithSettings'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {IconOpen} from 'sentry/icons/iconOpen'; import {t} from 'sentry/locale'; @@ -26,11 +31,6 @@ import {useQueryClient} from 'sentry/utils/queryClient'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useCanWriteSettings} from 'getsentry/views/seerAutomation/components/useCanWriteSettings'; -import {useBulkUpdateRepositorySettings} from 'getsentry/views/seerAutomation/onboarding/hooks/useBulkUpdateRepositorySettings'; -import { - getRepositoryWithSettingsQueryKey, - useRepositoryWithSettings, -} from 'getsentry/views/seerAutomation/onboarding/hooks/useRepositoryWithSettings'; interface Props { gridColumns: string; diff --git a/static/gsApp/views/seerAutomation/onboarding/configureCodeReviewStep.tsx b/static/gsApp/views/seerAutomation/onboarding/configureCodeReviewStep.tsx index 9aca9f09218163..abd5295a726169 100644 --- a/static/gsApp/views/seerAutomation/onboarding/configureCodeReviewStep.tsx +++ b/static/gsApp/views/seerAutomation/onboarding/configureCodeReviewStep.tsx @@ -16,13 +16,13 @@ import { } from 'sentry/components/guidedSteps/guidedSteps'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {PanelBody} from 'sentry/components/panels/panelBody'; +import {useBulkUpdateRepositorySettings} from 'sentry/components/repositories/useBulkUpdateRepositorySettings'; import {t} from 'sentry/locale'; import {DEFAULT_CODE_REVIEW_TRIGGERS} from 'sentry/types/integrations'; import {useOrganization} from 'sentry/utils/useOrganization'; import {trackGetsentryAnalytics} from 'getsentry/utils/trackGetsentryAnalytics'; import {useSeerOnboardingContext} from 'getsentry/views/seerAutomation/onboarding/hooks/seerOnboardingContext'; -import {useBulkUpdateRepositorySettings} from 'getsentry/views/seerAutomation/onboarding/hooks/useBulkUpdateRepositorySettings'; import {MaxWidthPanel, PanelDescription, StepContent} from './common'; import {RepositorySelector} from './repositorySelector'; diff --git a/static/gsApp/views/seerAutomation/repoDetails.tsx b/static/gsApp/views/seerAutomation/repoDetails.tsx index 8ad4bca596a086..2cfde0e8e7df2b 100644 --- a/static/gsApp/views/seerAutomation/repoDetails.tsx +++ b/static/gsApp/views/seerAutomation/repoDetails.tsx @@ -9,6 +9,7 @@ import {isSupportedAutofixProvider} from 'sentry/components/events/autofix/utils import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {RepoProviderIcon} from 'sentry/components/repositories/repoProviderIcon'; +import {useRepositoryWithSettings} from 'sentry/components/repositories/useRepositoryWithSettings'; import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; import {t, tct} from 'sentry/locale'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -17,7 +18,6 @@ import {SettingsPageHeader} from 'sentry/views/settings/components/settingsPageH import {RepoDetailsForm} from 'getsentry/views/seerAutomation/components/repoDetails/repoDetailsForm'; import {SeerSettingsPageWrapper} from 'getsentry/views/seerAutomation/components/seerSettingsPageWrapper'; -import {useRepositoryWithSettings} from 'getsentry/views/seerAutomation/onboarding/hooks/useRepositoryWithSettings'; export default function SeerRepoDetails() { const {repoId} = useParams<{repoId: string}>(); diff --git a/static/gsApp/views/seerAutomation/settings.tsx b/static/gsApp/views/seerAutomation/settings.tsx index e8c9881fd286f4..294ba0006eb8fb 100644 --- a/static/gsApp/views/seerAutomation/settings.tsx +++ b/static/gsApp/views/seerAutomation/settings.tsx @@ -15,6 +15,10 @@ import type {Organization} from 'sentry/types/organization'; import {fetchMutation} from 'sentry/utils/queryClient'; import {useOrganization} from 'sentry/utils/useOrganization'; import {SettingsPageHeader} from 'sentry/views/settings/components/settingsPageHeader'; +import { + CodeReviewOverviewSection, + useCodeReviewOverviewSection, +} from 'sentry/views/settings/seer/overview/codeReviewOverviewSection'; import { SCMOverviewSection, useSCMOverviewSection, @@ -37,7 +41,10 @@ export function SeerAutomationSettings() { const organization = useOrganization(); const canWrite = useCanWriteSettings(); + const showSeerOverview = true; // organization.features.includes('seer-overview'); + const scmOverviewData = useSCMOverviewSection(); + const codeReviewOverviewData = useCodeReviewOverviewSection(); const orgEndpoint = `/organizations/${organization.slug}/`; const orgMutationOpts = mutationOptions({ @@ -82,9 +89,9 @@ export function SeerAutomationSettings() { /> - - {t('Default Code Review for New Repos')} - , - } - )} - size="xs" - icon="info" - /> - - } - > - - {field => ( - - + ) : ( + + {t('Default Code Review for New Repos')} + , + } + )} + size="xs" + icon="info" /> - - )} - - } - mutationOptions={orgMutationOpts} > - {field => ( - } - )} - > - - - )} - - + + {field => ( + + + + )} + + + {field => ( + } + )} + > + + + )} + + + )} Date: Wed, 25 Mar 2026 17:39:15 -0700 Subject: [PATCH 2/8] bump stories and fix disabled prop --- .../codeReviewOverviewSection.stories.tsx | 137 ++++++++++++++++++ .../overview/codeReviewOverviewSection.tsx | 5 +- 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 static/app/views/settings/seer/overview/codeReviewOverviewSection.stories.tsx diff --git a/static/app/views/settings/seer/overview/codeReviewOverviewSection.stories.tsx b/static/app/views/settings/seer/overview/codeReviewOverviewSection.stories.tsx new file mode 100644 index 00000000000000..db2e47637cddfa --- /dev/null +++ b/static/app/views/settings/seer/overview/codeReviewOverviewSection.stories.tsx @@ -0,0 +1,137 @@ +import {Fragment, type ComponentProps} from 'react'; + +import * as Storybook from 'sentry/stories'; +import {RepositoryStatus} from 'sentry/types/integrations'; +import type {RepositoryWithSettings} from 'sentry/types/integrations'; +import type {Organization} from 'sentry/types/organization'; +import {CodeReviewOverviewSection} from 'sentry/views/settings/seer/overview/codeReviewOverviewSection'; + +type Props = ComponentProps; + +function makeRepo( + id: string, + name: string, + enabledCodeReview: boolean +): RepositoryWithSettings { + return { + id, + name, + externalId: id, + externalSlug: name, + integrationId: '1', + url: `https://github.com/${name}`, + status: RepositoryStatus.ACTIVE, + dateCreated: '', + provider: {id: 'integrations:github', name: 'GitHub'}, + settings: { + enabledCodeReview, + codeReviewTriggers: ['on_ready_for_review', 'on_new_commit'], + }, + }; +} + +const ALL_REPOS: RepositoryWithSettings[] = [ + makeRepo('1', 'my-org/frontend', false), + makeRepo('2', 'my-org/backend', false), + makeRepo('3', 'my-org/infra', false), + makeRepo('4', 'my-org/mobile', false), + makeRepo('5', 'my-org/data-pipeline', false), +]; + +const BASE_ORG = { + slug: 'my-org', + autoEnableCodeReview: true, + defaultCodeReviewTriggers: ['on_ready_for_review', 'on_new_commit'], + access: ['org:read', 'org:write', 'org:admin', 'org:integrations'], +} as Organization; + +const ORG = BASE_ORG; +const ORG_AUTO_OFF: Organization = {...BASE_ORG, autoEnableCodeReview: false}; +const ORG_READONLY: Organization = {...BASE_ORG, access: ['org:read']}; + +function makeProps( + seerRepos: RepositoryWithSettings[], + reposWithCodeReview: RepositoryWithSettings[], + orgOverride: Organization = ORG, + isPending = false +): Props { + return { + isPending, + data: {queryKey: ['repositories'] as any, seerRepos, reposWithCodeReview}, + refetch: () => Promise.resolve({} as any), + canWrite: true, + organization: orgOverride, + } as Props; +} + +export default Storybook.story('CodeReviewOverviewSection', story => { + story('Overview', () => ( + +

+ The is the code review + panel in the Seer settings overview page. It lets users enable or disable code + review across all existing repositories at once and configure the default triggers + (on push vs. on ready-for-review). +

+

+ The component derives its display from two lists: seerRepos (all + Seer-compatible repos) and reposWithCodeReview (the subset that have + code review enabled). The ratio between them drives the counter text and the + enable/disable bulk-action button state. The{' '} + Enable Code Review by Default toggle reflects{' '} + organization.autoEnableCodeReview and flips the direction of the bulk + action (enable remaining vs. disable all). +

+
+ )); + + story('Loading', () => ); + + story('0 repos', () => ); + + story('1 repo / 0 enabled', () => ( + + )); + + story('1 repo / 1 enabled (all)', () => ( + + )); + + story('N repos / 0 enabled', () => ( + + )); + + story('N repos / 1 enabled', () => ( + + )); + + story('N repos / many enabled (not all)', () => ( + + )); + + story('N repos / all enabled', () => ( + + )); + + story('Auto-enable off / 0 enabled', () => ( + + )); + + story('Auto-enable off / some enabled', () => ( + + )); + + story('Auto-enable off / all enabled', () => ( + + )); + + story('Read-only (no org:write)', () => ( + + )); +}); diff --git a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx index d9eae1ad4daf37..9f50aa2c02b3d8 100644 --- a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx +++ b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx @@ -219,9 +219,10 @@ export function CodeReviewOverviewSection({ size="xs" busy={isPending} disabled={ - !canWrite || field.state.value + !canWrite || + (field.state.value ? reposWithCodeReviewCount === seerReposCount - : seerReposCount - reposWithCodeReviewCount === seerReposCount + : seerReposCount - reposWithCodeReviewCount === seerReposCount) } onClick={() => { handleToggleCodeReview(field.state.value); From f3aec38f49a4f3fc17125a07237774f9e976ead7 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 26 Mar 2026 15:13:17 -0700 Subject: [PATCH 3/8] fix logic issue and label --- .../settings/seer/overview/codeReviewOverviewSection.tsx | 4 +++- static/gsApp/views/seerAutomation/settings.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx index 9f50aa2c02b3d8..a0b6a89d54c222 100644 --- a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx +++ b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx @@ -91,7 +91,9 @@ export function CodeReviewOverviewSection({ const {mutate: mutateRepositorySettings} = useBulkUpdateRepositorySettings({ onSettled: mutations => { // Invalidate the repositories query to get the updated settings - queryClient.invalidateQueries({queryKey}); + if (queryKey) { + queryClient.invalidateQueries({queryKey}); + } (mutations ?? []).forEach(mutation => { // Invalidate related queries queryClient.invalidateQueries({ diff --git a/static/gsApp/views/seerAutomation/settings.tsx b/static/gsApp/views/seerAutomation/settings.tsx index 294ba0006eb8fb..d2ba78a3f63d42 100644 --- a/static/gsApp/views/seerAutomation/settings.tsx +++ b/static/gsApp/views/seerAutomation/settings.tsx @@ -41,7 +41,7 @@ export function SeerAutomationSettings() { const organization = useOrganization(); const canWrite = useCanWriteSettings(); - const showSeerOverview = true; // organization.features.includes('seer-overview'); + const showSeerOverview = organization.features.includes('seer-overview'); const scmOverviewData = useSCMOverviewSection(); const codeReviewOverviewData = useCodeReviewOverviewSection(); From 714ffe89e23a66d7a8a0afd3745aaf28b0406db3 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 26 Mar 2026 15:14:27 -0700 Subject: [PATCH 4/8] fix dupe canWrite variable --- .../settings/seer/overview/codeReviewOverviewSection.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx index a0b6a89d54c222..47201a643cce94 100644 --- a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx +++ b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx @@ -11,7 +11,6 @@ import {Text} from '@sentry/scraps/text'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {updateOrganization} from 'sentry/actionCreators/organizations'; -import {hasEveryAccess} from 'sentry/components/acl/access'; import {organizationRepositoriesInfiniteOptions} from 'sentry/components/events/autofix/preferences/hooks/useOrganizationRepositories'; import {isSupportedAutofixProvider} from 'sentry/components/events/autofix/utils'; import {useBulkUpdateRepositorySettings} from 'sentry/components/repositories/useBulkUpdateRepositorySettings'; @@ -59,6 +58,7 @@ type Props = ReturnType & { }; export function CodeReviewOverviewSection({ + canWrite, isPending, organization, data, @@ -66,8 +66,6 @@ export function CodeReviewOverviewSection({ }: Props) { const queryClient = useQueryClient(); - const canWrite = hasEveryAccess(['org:write'], {organization}); - const {queryKey, seerRepos = [], reposWithCodeReview = []} = data ?? {}; const seerReposCount = seerRepos.length; From 2e0c667db010de348a900ad521c2ed2430706850 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 26 Mar 2026 15:20:37 -0700 Subject: [PATCH 5/8] remove useSeerOverviewData, i split this up and renamed --- .../overview/useSeerOverviewData.spec.tsx | 31 +++-- .../seer/overview/useSeerOverviewData.tsx | 106 ------------------ 2 files changed, 23 insertions(+), 114 deletions(-) delete mode 100644 static/app/views/settings/seer/overview/useSeerOverviewData.tsx diff --git a/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx b/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx index abfabb69dcd9d2..dc0c2d2ab595cf 100644 --- a/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx +++ b/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx @@ -95,13 +95,17 @@ describe('useSeerOverviewData', () => { expect(result.current.stats).toEqual({ integrationCount: 0, - totalRepoCount: 0, - seerRepoCount: 0, - reposWithSettingsCount: 0, - projectsWithReposCount: 0, projectsWithAutomationCount: 0, - totalProjects: 0, + projectsWithCreatePrCount: 0, + projectsWithReposCount: 0, reposWithCodeReviewCount: 0, + reposWithSettingsCount: 0, + scmIntegrationCount: 0, + seerIntegrationCount: 0, + seerIntegrations: expect.any(Array), + seerRepoCount: 0, + totalProjects: 0, + totalRepoCount: 0, }); }); @@ -138,9 +142,20 @@ describe('useSeerOverviewData', () => { await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.stats.totalRepoCount).toBe(3); - expect(result.current.stats.seerRepoCount).toBe(3); - expect(result.current.stats.integrationCount).toBe(2); // only integrations with 'commits' + expect(result.current.stats).toEqual({ + integrationCount: 3, + projectsWithAutomationCount: 0, + projectsWithCreatePrCount: 0, + projectsWithReposCount: 0, + reposWithCodeReviewCount: 0, + reposWithSettingsCount: 0, + scmIntegrationCount: 2, + seerIntegrationCount: 2, + seerIntegrations: expect.any(Array), + seerRepoCount: 3, + totalProjects: 0, + totalRepoCount: 3, + }); }); it('only counts repos with supported providers toward seerRepoCount', async () => { diff --git a/static/app/views/settings/seer/overview/useSeerOverviewData.tsx b/static/app/views/settings/seer/overview/useSeerOverviewData.tsx deleted file mode 100644 index 0770bc4c2229e3..00000000000000 --- a/static/app/views/settings/seer/overview/useSeerOverviewData.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import {useMemo} from 'react'; -import uniqBy from 'lodash/uniqBy'; - -import {bulkAutofixAutomationSettingsInfiniteOptions} from 'sentry/components/events/autofix/preferences/hooks/useBulkAutofixAutomationSettings'; -import {organizationRepositoriesInfiniteOptions} from 'sentry/components/events/autofix/preferences/hooks/useOrganizationRepositories'; -import {isSupportedAutofixProvider} from 'sentry/components/events/autofix/utils'; -import {organizationIntegrationsQueryOptions} from 'sentry/endpoints/organizations/organizationsIntegrationsQueryOptions'; -import {useFetchAllPages} from 'sentry/utils/api/apiFetch'; -import {useInfiniteQuery, useQuery} from 'sentry/utils/queryClient'; -import {useOrganization} from 'sentry/utils/useOrganization'; - -export function useSeerOverviewData() { - const organization = useOrganization(); - - // SCM Data - const {data: integrationData, isPending: isIntegrationsPending} = useQuery({ - ...organizationIntegrationsQueryOptions({organization}), - select: data => { - const allIntegrations = data.json.filter(i => i !== null); - const scmIntegrations = allIntegrations.filter(integration => - integration.provider.features.includes('commits') - ); - const seerIntegrations = scmIntegrations.filter(integration => - isSupportedAutofixProvider({ - id: integration.provider.key, - name: integration.provider.name, - }) - ); - return { - integrations: allIntegrations, - scmIntegrations, - seerIntegrations, - }; - }, - }); - - // Repos Data - const repositoriesResult = useInfiniteQuery({ - ...organizationRepositoriesInfiniteOptions({ - organization, - query: {per_page: 100}, - }), - select: ({pages}) => { - const allRepos = uniqBy( - pages.flatMap(page => page.json), - 'externalId' - ).filter(repository => repository.externalId); - const seerRepos = allRepos.filter(r => isSupportedAutofixProvider(r.provider)); - return { - allRepos, - seerRepos, - reposWithSettings: seerRepos.filter(r => r.settings !== null), - reposWithCodeReview: seerRepos.filter(r => r.settings?.enabledCodeReview), - }; - }, - }); - useFetchAllPages({result: repositoriesResult}); - const {data: repositoryData, isPending: isReposPending} = repositoriesResult; - - // Autofix Data - const autofixSettingsResult = useInfiniteQuery({ - ...bulkAutofixAutomationSettingsInfiniteOptions({organization}), - select: ({pages}) => { - const autofixItems = pages.flatMap(page => page.json).filter(s => s !== null); - return { - autofixItems, - projectsWithRepos: autofixItems.filter(settings => settings.reposCount > 0), - projectsWithAutomation: autofixItems.filter( - settings => settings.autofixAutomationTuning !== 'off' - ), - projectsWithCreatePr: autofixItems.filter( - settings => settings.automationHandoff?.auto_create_pr - ), - }; - }, - }); - useFetchAllPages({result: autofixSettingsResult}); - const {data: autofixData, isPending: isAutofixPending} = autofixSettingsResult; - - const stats = useMemo(() => { - return { - // SCM Stats - integrationCount: integrationData?.integrations.length ?? 0, - scmIntegrationCount: integrationData?.scmIntegrations.length ?? 0, - seerIntegrations: integrationData?.seerIntegrations ?? [], - seerIntegrationCount: integrationData?.seerIntegrations.length ?? 0, - - // Autofix Stats - totalProjects: autofixData?.autofixItems.length ?? 0, - projectsWithReposCount: autofixData?.projectsWithRepos.length ?? 0, - projectsWithAutomationCount: autofixData?.projectsWithAutomation.length ?? 0, - projectsWithCreatePrCount: autofixData?.projectsWithCreatePr.length ?? 0, - - // Repos Stats - totalRepoCount: repositoryData?.allRepos.length ?? 0, - seerRepoCount: repositoryData?.seerRepos.length ?? 0, - reposWithSettingsCount: repositoryData?.reposWithSettings.length ?? 0, - reposWithCodeReviewCount: repositoryData?.reposWithCodeReview.length ?? 0, - }; - }, [integrationData, autofixData, repositoryData]); - - return { - stats, - isLoading: isIntegrationsPending || isReposPending || isAutofixPending, - }; -} From 62c1094c6781647b0b819859e52054ee373ad390 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 26 Mar 2026 15:23:35 -0700 Subject: [PATCH 6/8] ref to use useSeerSupportedProviderIds --- .../settings/seer/overview/codeReviewOverviewSection.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx index 47201a643cce94..d3e2e643345e2c 100644 --- a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx +++ b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx @@ -12,7 +12,7 @@ import {Text} from '@sentry/scraps/text'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {updateOrganization} from 'sentry/actionCreators/organizations'; import {organizationRepositoriesInfiniteOptions} from 'sentry/components/events/autofix/preferences/hooks/useOrganizationRepositories'; -import {isSupportedAutofixProvider} from 'sentry/components/events/autofix/utils'; +import {useSeerSupportedProviderIds} from 'sentry/components/events/autofix/utils'; import {useBulkUpdateRepositorySettings} from 'sentry/components/repositories/useBulkUpdateRepositorySettings'; import {getRepositoryWithSettingsQueryKey} from 'sentry/components/repositories/useRepositoryWithSettings'; import {IconRefresh, IconSettings} from 'sentry/icons'; @@ -26,6 +26,7 @@ import {useOrganization} from 'sentry/utils/useOrganization'; export function useCodeReviewOverviewSection() { const organization = useOrganization(); + const seerSupportedProviderIds = useSeerSupportedProviderIds(); const queryOptions = organizationRepositoriesInfiniteOptions({ organization, @@ -38,7 +39,9 @@ export function useCodeReviewOverviewSection() { pages.flatMap(page => page.json), 'externalId' ).filter(repository => repository.externalId); - const seerRepos = repos.filter(r => isSupportedAutofixProvider(r.provider)); + const seerRepos = repos.filter(r => + seerSupportedProviderIds.includes(r.provider.id) + ); const reposWithCodeReview = seerRepos.filter(r => r.settings?.enabledCodeReview); return { queryKey: queryOptions.queryKey, From d3f142059879219e05a08efd8b7d9c5b7a84ee18 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 26 Mar 2026 15:25:50 -0700 Subject: [PATCH 7/8] remove unused test file, it was split and renamed --- .../overview/useSeerOverviewData.spec.tsx | 284 ------------------ 1 file changed, 284 deletions(-) delete mode 100644 static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx diff --git a/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx b/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx deleted file mode 100644 index dc0c2d2ab595cf..00000000000000 --- a/static/app/views/settings/seer/overview/useSeerOverviewData.spec.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import {OrganizationFixture} from 'sentry-fixture/organization'; -import {RepositoryFixture} from 'sentry-fixture/repository'; - -import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary'; - -import type { - OrganizationIntegration, - RepositoryWithSettings, -} from 'sentry/types/integrations'; -import {useSeerOverviewData} from 'sentry/views/settings/seer/overview/useSeerOverviewData'; - -function RepoWithSettingsFixture( - params: Partial = {} -): RepositoryWithSettings { - return { - ...RepositoryFixture(), - settings: null, - ...params, - }; -} - -function IntegrationFixture( - params: Partial & {features?: string[]} = {} -): OrganizationIntegration { - const {features = ['commits'], ...rest} = params; - return { - id: 'integration-1', - name: 'Test Integration', - domainName: 'github.com/test', - icon: null, - accountType: null, - gracePeriodEnd: null, - organizationIntegrationStatus: 'active', - status: 'active', - externalId: 'ext-integration-1', - organizationId: '1', - configData: null, - configOrganization: [], - provider: { - key: 'github', - slug: 'github', - name: 'GitHub', - canAdd: true, - canDisable: false, - features, - aspects: {}, - }, - ...rest, - }; -} - -describe('useSeerOverviewData', () => { - const organization = OrganizationFixture({slug: 'org-slug'}); - - afterEach(() => { - MockApiClient.clearMockResponses(); - }); - - function setupMocks({ - repos = [], - autofixSettings = [], - integrations = [], - }: { - autofixSettings?: Array<{ - autofixAutomationTuning: string | null; - projectId: string; - reposCount: number; - }>; - integrations?: OrganizationIntegration[]; - repos?: RepositoryWithSettings[]; - } = {}) { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/integrations/', - method: 'GET', - body: integrations, - }); - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/repos/', - method: 'GET', - body: repos, - }); - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/autofix/automation-settings/', - method: 'GET', - body: autofixSettings, - }); - } - - it('returns zeroed stats when there are no repos or projects', async () => { - setupMocks(); - - const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - expect(result.current.stats).toEqual({ - integrationCount: 0, - projectsWithAutomationCount: 0, - projectsWithCreatePrCount: 0, - projectsWithReposCount: 0, - reposWithCodeReviewCount: 0, - reposWithSettingsCount: 0, - scmIntegrationCount: 0, - seerIntegrationCount: 0, - seerIntegrations: expect.any(Array), - seerRepoCount: 0, - totalProjects: 0, - totalRepoCount: 0, - }); - }); - - it('counts repos and integrations with commits feature', async () => { - setupMocks({ - repos: [ - RepoWithSettingsFixture({ - id: '1', - externalId: 'ext-1', - integrationId: 'integration-a', - provider: {id: 'integrations:github', name: 'GitHub'}, - }), - RepoWithSettingsFixture({ - id: '2', - externalId: 'ext-2', - integrationId: 'integration-a', - provider: {id: 'integrations:github', name: 'GitHub'}, - }), - RepoWithSettingsFixture({ - id: '3', - externalId: 'ext-3', - integrationId: 'integration-b', - provider: {id: 'integrations:github', name: 'GitHub'}, - }), - ], - integrations: [ - IntegrationFixture({id: 'integration-a', features: ['commits', 'issue-basic']}), - IntegrationFixture({id: 'integration-b', features: ['commits']}), - IntegrationFixture({id: 'integration-c', features: ['issue-basic']}), // no commits - ], - }); - - const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - expect(result.current.stats).toEqual({ - integrationCount: 3, - projectsWithAutomationCount: 0, - projectsWithCreatePrCount: 0, - projectsWithReposCount: 0, - reposWithCodeReviewCount: 0, - reposWithSettingsCount: 0, - scmIntegrationCount: 2, - seerIntegrationCount: 2, - seerIntegrations: expect.any(Array), - seerRepoCount: 3, - totalProjects: 0, - totalRepoCount: 3, - }); - }); - - it('only counts repos with supported providers toward seerRepoCount', async () => { - setupMocks({ - repos: [ - RepoWithSettingsFixture({ - id: '1', - externalId: 'ext-1', - integrationId: 'integration-a', - provider: {id: 'integrations:github', name: 'GitHub'}, - }), - RepoWithSettingsFixture({ - id: '2', - externalId: 'ext-2', - integrationId: 'integration-b', - provider: {id: 'integrations:bitbucket', name: 'Bitbucket'}, // unsupported - }), - ], - }); - - const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - expect(result.current.stats.totalRepoCount).toBe(2); - expect(result.current.stats.seerRepoCount).toBe(1); - }); - - it('counts repos with code review enabled', async () => { - setupMocks({ - repos: [ - RepoWithSettingsFixture({ - id: '1', - externalId: 'ext-1', - integrationId: 'integration-a', - provider: {id: 'integrations:github', name: 'GitHub'}, - settings: {enabledCodeReview: true, codeReviewTriggers: []}, - }), - RepoWithSettingsFixture({ - id: '2', - externalId: 'ext-2', - integrationId: 'integration-a', - provider: {id: 'integrations:github', name: 'GitHub'}, - settings: {enabledCodeReview: false, codeReviewTriggers: []}, - }), - RepoWithSettingsFixture({ - id: '3', - externalId: 'ext-3', - integrationId: 'integration-a', - provider: {id: 'integrations:github', name: 'GitHub'}, - settings: null, // no settings - }), - ], - }); - - const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - expect(result.current.stats.reposWithCodeReviewCount).toBe(1); - }); - - it('counts projects with repos and with automation enabled', async () => { - setupMocks({ - autofixSettings: [ - {projectId: '1', reposCount: 2, autofixAutomationTuning: 'medium'}, - {projectId: '2', reposCount: 1, autofixAutomationTuning: 'off'}, - {projectId: '3', reposCount: 0, autofixAutomationTuning: 'off'}, - {projectId: '4', reposCount: 0, autofixAutomationTuning: 'medium'}, - ], - }); - - const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - expect(result.current.stats.totalProjects).toBe(4); - expect(result.current.stats.projectsWithReposCount).toBe(2); - expect(result.current.stats.projectsWithAutomationCount).toBe(2); - }); - - it('counts all non-off automation tuning values as enabled', async () => { - setupMocks({ - autofixSettings: [ - {projectId: '1', reposCount: 1, autofixAutomationTuning: 'medium'}, - {projectId: '2', reposCount: 1, autofixAutomationTuning: 'high'}, - {projectId: '3', reposCount: 1, autofixAutomationTuning: 'always'}, - {projectId: '4', reposCount: 0, autofixAutomationTuning: 'off'}, - {projectId: '5', reposCount: 0, autofixAutomationTuning: null}, - ], - }); - - const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - // only 'off' means disabled; null (deprecated) is also treated as enabled - expect(result.current.stats.projectsWithAutomationCount).toBe(4); - }); - - it('deduplicates repos by externalId', async () => { - setupMocks({ - repos: [ - RepoWithSettingsFixture({ - id: '1', - externalId: 'same-external-id', - integrationId: 'integration-a', - provider: {id: 'integrations:github', name: 'GitHub'}, - }), - RepoWithSettingsFixture({ - id: '2', - externalId: 'same-external-id', // duplicate - integrationId: 'integration-b', - provider: {id: 'integrations:github', name: 'GitHub'}, - }), - ], - }); - - const {result} = renderHookWithProviders(useSeerOverviewData, {organization}); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - expect(result.current.stats.totalRepoCount).toBe(1); - expect(result.current.stats.seerRepoCount).toBe(1); - }); -}); From 59e3337d1f1dbac54faff154dc5909a7b28b6822 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 26 Mar 2026 15:28:52 -0700 Subject: [PATCH 8/8] add codeowners for new files/folders --- .github/CODEOWNERS | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 41e942fbc7cd70..75e586ab5d82e3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -866,6 +866,10 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /tests/sentry/scm/ @getsentry/scm ## End of SCM +## SCM Frontend +/static/app/components/repositories @getsentry/coding-workflows-sentry-frontend @getsentry/scm +## End of SCM Frontend + # End of Coding Workflows # Conduit