feat(seer): Create an SCM config component to streamline seer setup#110166
feat(seer): Create an SCM config component to streamline seer setup#110166
Conversation
2e9ed29 to
d1feb32
Compare
d1feb32 to
7562bc0
Compare
Add a new SCM Config tab under Seer settings that provides a unified tree view for managing source code management integrations and repos. The page displays a virtualized three-level tree (Provider → Integration → Repository) allowing users to browse all SCM providers and their installed integrations, see which repos are connected, and add or remove repos directly from the tree. Includes search filtering, provider filtering (Seer-supported vs all), and repo status filtering (all, connected, disconnected). Key additions: - ScmIntegrationTree: virtualized tree component with expand/collapse - useScmIntegrationTreeData: data hook that fetches providers, integrations, connected repos (auto-paginated), and available repos per integration in parallel - ScmIntegrationTreeRow: renders provider, integration, repo, and empty-state rows with permission checks and confirmation dialogs - buildIntegrationTreeNodes: pure function to flatten the hierarchy into a filtered, virtualized list - New route at /settings/:org/seer/scm/ with tab navigation - Minor improvement to repositoryRow delete confirmation to include the repo name
7562bc0 to
b064d7a
Compare
| return ( | ||
| <Flex align="center" gap="sm"> | ||
| <Text size="sm" variant="muted"> | ||
| {messages[messageIndex]} |
There was a problem hiding this comment.
Bug: The loading message disappears after 30 seconds because the messageIndex goes out of bounds of the messages array.
Severity: MEDIUM
Suggested Fix
Use the modulo operator (%) to wrap the messageIndex around the length of the messages array. This will ensure the index always stays within valid bounds and the messages cycle indefinitely. For example: setMessageIndex(prev => (prev + 1) % messages.length).
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
static/gsApp/views/seerAutomation/components/scmIntegrationTree/scmIntegrationTreeRow.tsx#L333
Potential issue: The `LoadingReposMessage` component uses a `useTimeout` hook to cycle
through an array of 6 messages. The timeout reschedules itself indefinitely,
incrementing the `messageIndex` every 5 seconds. After 30 seconds, the index becomes 6,
which is out of the array's bounds. This causes `messages[messageIndex]` to evaluate to
`undefined`. React renders this as an empty string, making the loading message disappear
from the UI, even though the data is still loading. This is likely to happen for
organizations with many repositories, where loading can exceed 30 seconds.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Probably unlikely it takes 30 seconds to load but you never know
| icon={<IconAdd />} | ||
| disabled={!canAccess || node.isToggling} | ||
| onClick={() => onToggleRepo(node.repo, node.integration, false)} | ||
| aria-label={t('Add %s', <code>{node.repo.name}</code>)} |
There was a problem hiding this comment.
Bug: The aria-label for the 'Add' button incorrectly receives a React element, causing it to render as "[object Object]" for screen readers.
Severity: MEDIUM
Suggested Fix
Pass the repository name as a string directly to the translation function instead of wrapping it in a <code> element. The aria-label should be t('Add %s', node.repo.name).
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
static/gsApp/views/seerAutomation/components/scmIntegrationTree/scmIntegrationTreeRow.tsx#L299
Potential issue: The `aria-label` for the 'Add' button is constructed using `t('Add %s',
`{node.repo.name}`)`. The `t()` function's second argument is a React element
(`<code>`). When a React element is passed to a prop that expects a string, like
`aria-label`, React serializes it to the string `"[object Object]"`. This results in a
non-descriptive label for screen reader users, harming accessibility.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| const {start} = useTimeout({ | ||
| timeMs: 5000, | ||
| onTimeout: () => { | ||
| setMessageIndex(prev => prev + 1); |
There was a problem hiding this comment.
Loading message index exceeds array bounds after timeout
Medium Severity
In LoadingReposMessage, messageIndex increments unboundedly every 5 seconds via setMessageIndex(prev => prev + 1), but the messages array only has 6 elements (indices 0–5). After 30 seconds, messages[6] is undefined, causing the loading text to silently disappear while the spinner remains. The index needs to be clamped or wrapped (e.g., modulo the array length).
Additional Locations (1)
| icon={<IconAdd />} | ||
| disabled={!canAccess || node.isToggling} | ||
| onClick={() => onToggleRepo(node.repo, node.integration, false)} | ||
| aria-label={t('Add %s', <code>{node.repo.name}</code>)} |
There was a problem hiding this comment.
JSX in aria-label produces garbled accessibility text
Medium Severity
The aria-label at line 299 passes a JSX <code> element to t('Add %s', ...). The t() function returns a React node array when given React elements, but aria-label expects a string. This results in [object Object] as the accessibility label. Compare with line 283, which correctly passes the plain string node.repo.name.
| message={t( | ||
| 'Are you sure you want to remove this repository? All associated commit data will be removed in addition to the repository.' | ||
| 'Are you sure you want to remove %s? All associated commit data will be removed in addition to the repository.', | ||
| <code>{repository.name}</code> |
There was a problem hiding this comment.
Does this work or do we need tct?
|
|
||
| if (scmProviders.length === 0) { | ||
| return ( | ||
| <Flex direction="column" align="center" gap="md" padding="xl" minHeight={200}> |
There was a problem hiding this comment.
minHeight as prop here vs as inline style above for isPending and isError
| ); | ||
| addSuccessMessage(t('Removed %s', repo.name)); | ||
| } else { | ||
| const newRepo = await addRepository( |
| icon={<IconAdd />} | ||
| disabled={!canAccess || node.isToggling} | ||
| onClick={() => onToggleRepo(node.repo, node.integration, false)} | ||
| aria-label={t('Add %s', <code>{node.repo.name}</code>)} |
| return ( | ||
| <Flex align="center" gap="sm"> | ||
| <Text size="sm" variant="muted"> | ||
| {messages[messageIndex]} |
There was a problem hiding this comment.
Probably unlikely it takes 30 seconds to load but you never know
| } | ||
| } | ||
|
|
||
| // nodes.push({type: 'add-config', provider}); |


Add a new SCM Config tab under Seer settings that provides a unified
tree view for managing source code management integrations and repos.
The page displays a virtualized three-level tree (Provider → Integration
→ Repository) allowing users to browse all SCM providers and their
installed integrations, see which repos are connected, and add or remove
repos directly from the tree. Includes search filtering, provider
filtering (Seer-supported vs all), and repo status filtering (all,
connected, disconnected).
Key additions:
ScmIntegrationTree: virtualized tree component with expand/collapseuseScmIntegrationTreeData: data hook that fetches providers,integrations, connected repos (auto-paginated), and available repos
per integration in parallel
ScmIntegrationTreeRow: renders provider, integration, repo, andempty-state rows with permission checks and confirmation dialogs
buildIntegrationTreeNodes: pure function to flatten the hierarchyinto a filtered, virtualized list
repositoryRowdelete confirmation to includethe repo name
Seer SCM Config — Source Code Manager Integration Tree
2026-03-09T18:57:23Z by Showboat 0.6.1
What this PR does
This branch adds a new Seer SCM Config settings page that lets organization admins connect their source code repositories to Seer (Sentry's AI engine).
Seer needs read access to source code to:
Before this change, there was no dedicated UI to manage which repos are available to Seer. This PR introduces a filterable, virtualized tree that shows all SCM providers, their installed integrations, and the repos available under each — with one-click connect/disconnect toggles.
Files changed
scm.tsxscmIntegrationTree.tsxscmIntegrationTreeRow.tsxscmIntegrationTreeNodes.tsuseScmIntegrationTreeData.tstypes.tssettingsPageTabs.tsxroutes.tsx/settings/:orgId/seer/scm/routerepositoryRow.tsxaddRepositoryaction)Architecture
Data fetching (
useScmIntegrationTreeData)Four parallel queries, all resolved before the tree renders:
/organizations/:org/config/integrations/filtered to providers whosefeatureGateincludescommits(i.e. SCM providers)./organizations/:org/integrations/filtered to the SCM providers above./organizations/:org/repos/fetched with auto-pagination (infinite query that chaseshasNextPage)./organizations/:org/integrations/:id/repos/fired in parallel for each installed integration viauseQueries.The hook returns a stable
connectedIdentifiersSet (keyed onrepo.name, notexternalSlug, because GitLab uses a numeric project ID as the slug which never matches theidentifierfield onIntegrationRepository).Node model (
types.ts+scmIntegrationTreeNodes.ts)A flat array of
TreeNodeobjects (a discriminated union) is built from the fetched data. The tree looks like:buildIntegrationTreeNodesapplies three filters before emittingRepoNodes:seer-supported(default) hides providers not in a supported allowlist;allshows everythingconnected/not-connected/allrepo.identifierVirtualisation
The flat node array is passed to
@tanstack/react-virtual'suseVirtualizerso that only the visible rows are in the DOM. Each row is a fixed56pxheight.Row toggle logic
Connecting a repo calls
addRepository(existing action creator), disconnecting callshideRepository. Both optimistically update theconnectedIdentifiersset in the react-query cache to give immediate feedback. The checkbox is disabled while the mutation is in-flight (togglingReposstate).Key design decisions
1. Flat list + virtual scroll over recursive tree
A nested tree renders every node in the DOM, which breaks down with hundreds of repos per integration. The flat
TreeNode[]array letsuseVirtualizeronly mount visible rows. Expand/collapse just re-runsbuildIntegrationTreeNodes, which is O(n) and synchronous.2.
repo.name(notexternalSlug) for connection matchingGitLab's
externalSlugis a numeric project ID (e.g.12345) whileIntegrationRepository.identifieris alwaysowner/repo. Usingrepo.namegives a consistentowner/repokey across GitHub, GitLab, and Bitbucket.3.
seer-supportedprovider filter as defaultThe default view hides obscure or legacy SCM providers that Seer doesn't actually use. Users can switch to
All providersif they need to manage a non-standard integration.4. URL-persisted filters via
nuqsSearch term, provider filter, and repo filter are stored in the URL query string using
nuqs(parseAsString,parseAsStringLiteral). This means filter state survives page refresh and can be linked.5. Optimistic cache update on connect/disconnect
After calling
addRepository/hideRepository, the tree immediately reflects the new connected state by mutating the react-query cache for the repos list. The toggle button is disabled while in-flight to prevent double-clicks.Reviewer checklist
useScmIntegrationTreeDatamakes 4 parallel requests on mount — check network tab for any waterfallNoMatchNodemessage renders per-integrationKnown limitations
/repos/endpoint returns all repos for the integration, which could be large for orgs with many repos in a single integration)seer-supportedallowlist is maintained as a frontend constant — new providers require a code change