Skip to content

Commit 71f5b48

Browse files
authored
ref(onboarding): Move less common SCM providers into a More dropdown (#112730)
## Summary - Split the SCM provider selector into primary pills (GitHub, GitLab, Bitbucket) and a "More" dropdown for less common providers (Bitbucket Server, GitHub Enterprise, Azure DevOps) - Reduces visual noise on the connect step Stacks on #112696. Refs VDY-69 ## Test plan - [ ] Verify primary providers render as top-level pill buttons - [ ] Verify secondary providers appear in the "More" dropdown - [ ] Verify clicking a dropdown item triggers the OAuth install flow - [ ] Verify "More" dropdown does not appear when only primary providers exist
1 parent 684d8c0 commit 71f5b48

File tree

2 files changed

+181
-24
lines changed

2 files changed

+181
-24
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {GitHubIntegrationProviderFixture} from 'sentry-fixture/githubIntegrationProvider';
2+
import {GitLabIntegrationProviderFixture} from 'sentry-fixture/gitlabIntegrationProvider';
3+
4+
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
5+
6+
import {ScmProviderPills} from './scmProviderPills';
7+
8+
const bitbucketProvider = GitHubIntegrationProviderFixture({
9+
key: 'bitbucket',
10+
slug: 'bitbucket',
11+
name: 'Bitbucket',
12+
});
13+
14+
const bitbucketServerProvider = GitHubIntegrationProviderFixture({
15+
key: 'bitbucket_server',
16+
slug: 'bitbucket_server',
17+
name: 'Bitbucket Server',
18+
});
19+
20+
const gitHubEnterpriseProvider = GitHubIntegrationProviderFixture({
21+
key: 'github_enterprise',
22+
slug: 'github_enterprise',
23+
name: 'GitHub Enterprise',
24+
});
25+
26+
const azureDevOpsProvider = GitHubIntegrationProviderFixture({
27+
key: 'vsts',
28+
slug: 'vsts',
29+
name: 'Azure DevOps',
30+
});
31+
32+
describe('ScmProviderPills', () => {
33+
it('renders primary providers as top-level buttons', () => {
34+
const providers = [
35+
GitHubIntegrationProviderFixture(),
36+
GitLabIntegrationProviderFixture(),
37+
bitbucketProvider,
38+
];
39+
40+
render(<ScmProviderPills providers={providers} onInstall={jest.fn()} />);
41+
42+
expect(screen.getByText('GitHub')).toBeInTheDocument();
43+
expect(screen.getByText('GitLab')).toBeInTheDocument();
44+
expect(screen.getByText('Bitbucket')).toBeInTheDocument();
45+
expect(screen.queryByText('More')).not.toBeInTheDocument();
46+
});
47+
48+
it('shows secondary providers in a "More" dropdown', async () => {
49+
const providers = [
50+
GitHubIntegrationProviderFixture(),
51+
GitLabIntegrationProviderFixture(),
52+
bitbucketProvider,
53+
bitbucketServerProvider,
54+
gitHubEnterpriseProvider,
55+
azureDevOpsProvider,
56+
];
57+
58+
render(<ScmProviderPills providers={providers} onInstall={jest.fn()} />);
59+
60+
// Primary providers are visible as buttons
61+
expect(screen.getByText('GitHub')).toBeInTheDocument();
62+
expect(screen.getByText('GitLab')).toBeInTheDocument();
63+
expect(screen.getByText('Bitbucket')).toBeInTheDocument();
64+
65+
// Secondary providers are hidden behind the "More" dropdown
66+
expect(screen.queryByText('Bitbucket Server')).not.toBeInTheDocument();
67+
68+
await userEvent.click(screen.getByRole('button', {name: 'More'}));
69+
70+
expect(
71+
screen.getByRole('menuitemradio', {name: 'Bitbucket Server'})
72+
).toBeInTheDocument();
73+
expect(
74+
screen.getByRole('menuitemradio', {name: 'GitHub Enterprise'})
75+
).toBeInTheDocument();
76+
expect(screen.getByRole('menuitemradio', {name: 'Azure DevOps'})).toBeInTheDocument();
77+
});
78+
79+
it('triggers install flow when clicking a dropdown item', async () => {
80+
const open = jest.spyOn(window, 'open').mockReturnValue({
81+
focus: jest.fn(),
82+
close: jest.fn(),
83+
} as any);
84+
85+
const providers = [GitHubIntegrationProviderFixture(), gitHubEnterpriseProvider];
86+
87+
render(<ScmProviderPills providers={providers} onInstall={jest.fn()} />);
88+
89+
await userEvent.click(screen.getByRole('button', {name: 'More'}));
90+
await userEvent.click(screen.getByRole('menuitemradio', {name: 'GitHub Enterprise'}));
91+
92+
expect(open).toHaveBeenCalledTimes(1);
93+
});
94+
95+
it('does not render "More" dropdown when all providers are primary', () => {
96+
const providers = [GitHubIntegrationProviderFixture()];
97+
98+
render(<ScmProviderPills providers={providers} onInstall={jest.fn()} />);
99+
100+
expect(screen.getByText('GitHub')).toBeInTheDocument();
101+
expect(screen.queryByText('More')).not.toBeInTheDocument();
102+
});
103+
});
Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,96 @@
1-
import {Flex} from '@sentry/scraps/layout';
1+
import {Flex, Grid} from '@sentry/scraps/layout';
22

3+
import {DropdownMenu} from 'sentry/components/dropdownMenu';
4+
import {t} from 'sentry/locale';
35
import type {Integration, IntegrationProvider} from 'sentry/types/integrations';
6+
import {useAddIntegration} from 'sentry/utils/integrations/useAddIntegration';
47
import {getIntegrationIcon} from 'sentry/utils/integrationUtil';
8+
import {useOrganization} from 'sentry/utils/useOrganization';
59
import {IntegrationButton} from 'sentry/views/settings/organizationIntegrations/integrationButton';
610
import {IntegrationContext} from 'sentry/views/settings/organizationIntegrations/integrationContext';
711

12+
/**
13+
* Provider keys shown as top-level pill buttons. Everything else is grouped
14+
* into the "More" dropdown to reduce visual clutter.
15+
*/
16+
const PRIMARY_PROVIDER_KEYS = new Set(['github', 'gitlab', 'bitbucket']);
17+
818
interface ScmProviderPillsProps {
919
onInstall: (data: Integration) => void;
1020
providers: IntegrationProvider[];
1121
}
1222

1323
export function ScmProviderPills({providers, onInstall}: ScmProviderPillsProps) {
24+
const organization = useOrganization();
25+
const {startFlow} = useAddIntegration();
26+
const primaryProviders = providers.filter(p => PRIMARY_PROVIDER_KEYS.has(p.key));
27+
const moreProviders = providers.filter(p => !PRIMARY_PROVIDER_KEYS.has(p.key));
28+
const gridItemCount = primaryProviders.length + (moreProviders.length > 0 ? 1 : 0);
29+
30+
const columnsXs = `repeat(${Math.min(gridItemCount, 2)}, 1fr)`;
31+
const columnsMd = [
32+
primaryProviders.length && `repeat(${primaryProviders.length}, 1fr)`,
33+
moreProviders.length && 'min-content',
34+
]
35+
.filter(Boolean)
36+
.join(' ');
37+
1438
return (
15-
<Flex gap="lg" wrap="wrap" justify="center">
16-
{providers.map(provider => (
17-
<IntegrationContext
18-
key={provider.key}
19-
value={{
20-
provider,
21-
type: 'first_party',
22-
installStatus: 'Not Installed',
23-
analyticsParams: {
24-
view: 'onboarding_scm',
25-
already_installed: false,
26-
},
27-
}}
28-
>
29-
<IntegrationButton
30-
userHasAccess
31-
onAddIntegration={onInstall}
32-
onExternalClick={() => {}}
33-
buttonProps={{
34-
icon: getIntegrationIcon(provider.key, 'sm'),
35-
buttonText: provider.name,
39+
<Flex justify="center">
40+
<Grid
41+
columns={{
42+
xs: columnsXs,
43+
md: columnsMd,
44+
}}
45+
justify="center"
46+
gap="lg"
47+
>
48+
{primaryProviders.map(provider => (
49+
<IntegrationContext
50+
key={provider.key}
51+
value={{
52+
provider,
53+
type: 'first_party',
54+
installStatus: 'Not Installed',
55+
analyticsParams: {
56+
view: 'onboarding_scm',
57+
already_installed: false,
58+
},
3659
}}
60+
>
61+
<IntegrationButton
62+
userHasAccess
63+
onAddIntegration={onInstall}
64+
onExternalClick={() => {}}
65+
buttonProps={{
66+
icon: getIntegrationIcon(provider.key, 'sm'),
67+
buttonText: provider.name,
68+
}}
69+
/>
70+
</IntegrationContext>
71+
))}
72+
{moreProviders.length > 0 && (
73+
<DropdownMenu
74+
triggerLabel={t('More')}
75+
position="bottom-end"
76+
items={moreProviders.map(provider => ({
77+
key: provider.key,
78+
label: provider.name,
79+
leadingItems: getIntegrationIcon(provider.key, 'sm'),
80+
onAction: () =>
81+
startFlow({
82+
provider,
83+
organization,
84+
onInstall,
85+
analyticsParams: {
86+
view: 'onboarding_scm',
87+
already_installed: false,
88+
},
89+
}),
90+
}))}
3791
/>
38-
</IntegrationContext>
39-
))}
92+
)}
93+
</Grid>
4094
</Flex>
4195
);
4296
}

0 commit comments

Comments
 (0)