Browse Source

SONAR-17587 Allow project onboarding when multiple Github integrations are configured

tags/9.8.0.63668
Revanshu Paliwal 1 year ago
parent
commit
7fe3240e62
23 changed files with 268 additions and 144 deletions
  1. 28
    0
      server/sonar-web/src/main/js/api/mocks/AlmIntegrationsServiceMock.ts
  2. 2
    0
      server/sonar-web/src/main/js/api/mocks/AlmSettingsServiceMock.ts
  3. 2
    2
      server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx
  4. 2
    2
      server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx
  5. 3
    3
      server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
  6. 59
    31
      server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx
  7. 17
    2
      server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx
  8. 2
    2
      server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx
  9. 2
    2
      server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx
  10. 1
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx
  11. 36
    3
      server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProject-it.tsx
  12. 4
    4
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx
  13. 3
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx
  14. 1
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx
  15. 1
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap
  16. 6
    36
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap
  17. 1
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
  18. 90
    48
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap
  19. 1
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap
  20. 1
    1
      server/sonar-web/src/main/js/apps/create/project/constants.ts
  21. 1
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx
  22. 2
    0
      server/sonar-web/src/main/js/components/devops-platform/AlmSettingsInstanceSelector.tsx
  23. 3
    2
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 28
- 0
server/sonar-web/src/main/js/api/mocks/AlmIntegrationsServiceMock.ts View File

import { GitlabProject } from '../../types/alm-integration'; import { GitlabProject } from '../../types/alm-integration';
import { import {
checkPersonalAccessTokenIsValid, checkPersonalAccessTokenIsValid,
getGithubClientId,
getGithubOrganizations,
getGitlabProjects, getGitlabProjects,
setAlmPersonalAccessToken, setAlmPersonalAccessToken,
} from '../alm-integrations'; } from '../alm-integrations';
defaultAlmInstancePATMap: { [key: string]: boolean } = { defaultAlmInstancePATMap: { [key: string]: boolean } = {
'conf-final-1': false, 'conf-final-1': false,
'conf-final-2': true, 'conf-final-2': true,
'conf-github-1': false,
'conf-github-2': true,
}; };


defaultGitlabProjects: GitlabProject[] = [ defaultGitlabProjects: GitlabProject[] = [
mockGitlabProject({ name: 'Gitlab project 3', id: '3' }), mockGitlabProject({ name: 'Gitlab project 3', id: '3' }),
]; ];


defaultOrganizations = {
paging: {
pageIndex: 1,
pageSize: 100,
total: 1,
},
organizations: [
{
key: 'org-1',
name: 'org-1',
},
],
};

constructor() { constructor() {
this.almInstancePATMap = cloneDeep(this.defaultAlmInstancePATMap); this.almInstancePATMap = cloneDeep(this.defaultAlmInstancePATMap);
this.gitlabProjects = cloneDeep(this.defaultGitlabProjects); this.gitlabProjects = cloneDeep(this.defaultGitlabProjects);
); );
(setAlmPersonalAccessToken as jest.Mock).mockImplementation(this.setAlmPersonalAccessToken); (setAlmPersonalAccessToken as jest.Mock).mockImplementation(this.setAlmPersonalAccessToken);
(getGitlabProjects as jest.Mock).mockImplementation(this.getGitlabProjects); (getGitlabProjects as jest.Mock).mockImplementation(this.getGitlabProjects);
(getGithubClientId as jest.Mock).mockImplementation(this.getGithubClientId);
(getGithubOrganizations as jest.Mock).mockImplementation(this.getGithubOrganizations);
} }


checkPersonalAccessTokenIsValid = (conf: string) => { checkPersonalAccessTokenIsValid = (conf: string) => {
this.gitlabProjects = gitlabProjects; this.gitlabProjects = gitlabProjects;
} }


getGithubClientId = () => {
return Promise.resolve({ clientId: 'clientId' });
};

getGithubOrganizations = () => {
return Promise.resolve(this.defaultOrganizations);
};

reset = () => { reset = () => {
this.almInstancePATMap = cloneDeep(this.defaultAlmInstancePATMap); this.almInstancePATMap = cloneDeep(this.defaultAlmInstancePATMap);
this.gitlabProjects = cloneDeep(this.defaultGitlabProjects); this.gitlabProjects = cloneDeep(this.defaultGitlabProjects);

+ 2
- 0
server/sonar-web/src/main/js/api/mocks/AlmSettingsServiceMock.ts View File

defaultSetting: AlmSettingsInstance[] = [ defaultSetting: AlmSettingsInstance[] = [
mockAlmSettingsInstance({ key: 'conf-final-1', alm: AlmKeys.GitLab }), mockAlmSettingsInstance({ key: 'conf-final-1', alm: AlmKeys.GitLab }),
mockAlmSettingsInstance({ key: 'conf-final-2', alm: AlmKeys.GitLab }), mockAlmSettingsInstance({ key: 'conf-final-2', alm: AlmKeys.GitLab }),
mockAlmSettingsInstance({ key: 'conf-github-1', alm: AlmKeys.GitHub, url: 'url' }),
mockAlmSettingsInstance({ key: 'conf-github-2', alm: AlmKeys.GitHub, url: 'url' }),
]; ];


constructor() { constructor() {

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx View File

} }
}; };


onChangeConfig = (instance: AlmSettingsInstance) => {
onSelectedAlmInstanceChange = (instance: AlmSettingsInstance) => {
this.setState({ selectedAlmInstance: instance }, () => this.fetchData()); this.setState({ selectedAlmInstance: instance }, () => this.fetchData());
}; };


showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)} showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}
submittingToken={submittingToken} submittingToken={submittingToken}
tokenValidationFailed={tokenValidationFailed} tokenValidationFailed={tokenValidationFailed}
onChangeConfig={this.onChangeConfig}
onSelectedAlmInstanceChange={this.onSelectedAlmInstanceChange}
/> />
); );
} }

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx View File

showPersonalAccessTokenForm?: boolean; showPersonalAccessTokenForm?: boolean;
submittingToken?: boolean; submittingToken?: boolean;
tokenValidationFailed: boolean; tokenValidationFailed: boolean;
onChangeConfig: (instance: AlmSettingsInstance) => void;
onSelectedAlmInstanceChange: (instance: AlmSettingsInstance) => void;
} }


export default function AzureProjectCreateRenderer(props: AzureProjectCreateRendererProps) { export default function AzureProjectCreateRenderer(props: AzureProjectCreateRendererProps) {
<AlmSettingsInstanceDropdown <AlmSettingsInstanceDropdown
almInstances={almInstances} almInstances={almInstances}
selectedAlmInstance={selectedAlmInstance} selectedAlmInstance={selectedAlmInstance}
onChangeConfig={props.onChangeConfig}
onChangeConfig={props.onSelectedAlmInstanceChange}
/> />


{loading && <i className="spinner" />} {loading && <i className="spinner" />}

+ 3
- 3
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx View File

import './style.css'; import './style.css';
import { CreateProjectModes } from './types'; import { CreateProjectModes } from './types';


interface Props extends WithAvailableFeaturesProps {
export interface CreateProjectPageProps extends WithAvailableFeaturesProps {
appState: AppState; appState: AppState;
location: Location; location: Location;
router: Router; router: Router;
[AlmKeys.GitLab]: CreateProjectModes.GitLab, [AlmKeys.GitLab]: CreateProjectModes.GitLab,
}; };


export class CreateProjectPage extends React.PureComponent<Props, State> {
export class CreateProjectPage extends React.PureComponent<CreateProjectPageProps, State> {
mounted = false; mounted = false;
state: State = { state: State = {
azureSettings: [], azureSettings: [],
location={location} location={location}
onProjectCreate={this.handleProjectCreate} onProjectCreate={this.handleProjectCreate}
router={router} router={router}
settings={githubSettings}
almInstances={githubSettings}
/> />
); );
} }

+ 59
- 31
server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx View File

canAdmin: boolean; canAdmin: boolean;
loadingBindings: boolean; loadingBindings: boolean;
onProjectCreate: (projectKey: string) => void; onProjectCreate: (projectKey: string) => void;
settings: AlmSettingsInstance[];
almInstances: AlmSettingsInstance[];
location: Location; location: Location;
router: Router; router: Router;
} }
searchQuery: string; searchQuery: string;
selectedOrganization?: GithubOrganization; selectedOrganization?: GithubOrganization;
selectedRepository?: GithubRepository; selectedRepository?: GithubRepository;
settings?: AlmSettingsInstance;
selectedAlmInstance?: AlmSettingsInstance;
} }


const REPOSITORY_PAGE_SIZE = 30; const REPOSITORY_PAGE_SIZE = 30;
repositories: [], repositories: [],
repositoryPaging: { pageSize: REPOSITORY_PAGE_SIZE, total: 0, pageIndex: 1 }, repositoryPaging: { pageSize: REPOSITORY_PAGE_SIZE, total: 0, pageIndex: 1 },
searchQuery: '', searchQuery: '',
settings: props.settings[0],
selectedAlmInstance: this.getInitialSelectedAlmInstance(),
}; };


this.triggerSearch = debounce(this.triggerSearch, 250); this.triggerSearch = debounce(this.triggerSearch, 250);


componentDidMount() { componentDidMount() {
this.mounted = true; this.mounted = true;

this.initialize(); this.initialize();
} }


componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
if (prevProps.settings.length === 0 && this.props.settings.length > 0) {
this.setState({ settings: this.props.settings[0] }, () => this.initialize());
if (prevProps.almInstances.length === 0 && this.props.almInstances.length > 0) {
this.setState({ selectedAlmInstance: this.getInitialSelectedAlmInstance() }, () =>
this.initialize()
);
} }
} }


this.mounted = false; this.mounted = false;
} }


getInitialSelectedAlmInstance() {
const {
location: {
query: { almInstance: selectedAlmInstanceKey },
},
almInstances,
} = this.props;
const selectedAlmInstance = almInstances.find(
(instance) => instance.key === selectedAlmInstanceKey
);
if (selectedAlmInstance) {
return selectedAlmInstance;
}
return this.props.almInstances.length > 1 ? undefined : this.props.almInstances[0];
}

async initialize() { async initialize() {
const { location, router } = this.props; const { location, router } = this.props;
const { settings } = this.state;

if (!settings || !settings.url) {
const { selectedAlmInstance } = this.state;
if (!selectedAlmInstance || !selectedAlmInstance.url) {
this.setState({ error: true }); this.setState({ error: true });
return; return;
} else {
this.setState({ error: false });
} }
this.setState({ error: false });


const code = location.query?.code; const code = location.query?.code;

try { try {
if (!code) { if (!code) {
await this.redirectToGithub(settings);
await this.redirectToGithub(selectedAlmInstance);
} else { } else {
delete location.query.code; delete location.query.code;
router.replace(location); router.replace(location);
await this.fetchOrganizations(settings, code);
await this.fetchOrganizations(selectedAlmInstance, code);
} }
} catch (e) { } catch (e) {
if (this.mounted) { if (this.mounted) {
} }
} }


async redirectToGithub(settings: AlmSettingsInstance) {
if (!settings.url) {
async redirectToGithub(selectedAlmInstance: AlmSettingsInstance) {
if (!selectedAlmInstance.url) {
return; return;
} }


const { clientId } = await getGithubClientId(settings.key);
const { clientId } = await getGithubClientId(selectedAlmInstance.key);


if (!clientId) { if (!clientId) {
this.setState({ error: true }); this.setState({ error: true });
return; return;
} }

const queryParams = [ const queryParams = [
{ param: 'client_id', value: clientId }, { param: 'client_id', value: clientId },
{ param: 'redirect_uri', value: `${getHostUrl()}/projects/create?mode=${AlmKeys.GitHub}` },
{
param: 'redirect_uri',
value: encodeURIComponent(
`${getHostUrl()}/projects/create?mode=${AlmKeys.GitHub}&almInstance=${
selectedAlmInstance.key
}`
),
},
] ]
.map(({ param, value }) => `${param}=${value}`) .map(({ param, value }) => `${param}=${value}`)
.join('&'); .join('&');


let instanceRootUrl; let instanceRootUrl;
// Strip the api section from the url, since we're not hitting the api here. // Strip the api section from the url, since we're not hitting the api here.
if (settings.url.includes('/api/v3')) {
if (selectedAlmInstance.url.includes('/api/v3')) {
// GitHub Enterprise // GitHub Enterprise
instanceRootUrl = settings.url.replace('/api/v3', '');
instanceRootUrl = selectedAlmInstance.url.replace('/api/v3', '');
} else { } else {
// github.com // github.com
instanceRootUrl = settings.url.replace('api.', '');
instanceRootUrl = selectedAlmInstance.url.replace('api.', '');
} }


// strip the trailing / // strip the trailing /
window.location.replace(`${instanceRootUrl}/login/oauth/authorize?${queryParams}`); window.location.replace(`${instanceRootUrl}/login/oauth/authorize?${queryParams}`);
} }


async fetchOrganizations(settings: AlmSettingsInstance, token: string) {
const { organizations } = await getGithubOrganizations(settings.key, token);
async fetchOrganizations(selectedAlmInstance: AlmSettingsInstance, token: string) {
const { organizations } = await getGithubOrganizations(selectedAlmInstance.key, token);


if (this.mounted) { if (this.mounted) {
this.setState({ loadingOrganizations: false, organizations }); this.setState({ loadingOrganizations: false, organizations });


async fetchRepositories(params: { organizationKey: string; page?: number; query?: string }) { async fetchRepositories(params: { organizationKey: string; page?: number; query?: string }) {
const { organizationKey, page = 1, query } = params; const { organizationKey, page = 1, query } = params;
const { settings } = this.state;
const { selectedAlmInstance } = this.state;


if (!settings) {
if (!selectedAlmInstance) {
this.setState({ error: true }); this.setState({ error: true });
return; return;
} }


try { try {
const data = await getGithubRepositories({ const data = await getGithubRepositories({
almSetting: settings.key,
almSetting: selectedAlmInstance.key,
organization: organizationKey, organization: organizationKey,
pageSize: REPOSITORY_PAGE_SIZE, pageSize: REPOSITORY_PAGE_SIZE,
page, page,
}; };


handleImportRepository = async () => { handleImportRepository = async () => {
const { selectedOrganization, selectedRepository, settings } = this.state;
const { selectedOrganization, selectedRepository, selectedAlmInstance } = this.state;


if (settings && selectedOrganization && selectedRepository) {
if (selectedAlmInstance && selectedOrganization && selectedRepository) {
this.setState({ importing: true }); this.setState({ importing: true });


try { try {
const { project } = await importGithubRepository( const { project } = await importGithubRepository(
settings.key,
selectedAlmInstance.key,
selectedOrganization.key, selectedOrganization.key,
selectedRepository.key selectedRepository.key
); );
} }
}; };


onSelectedAlmInstanceChange = (instance: AlmSettingsInstance) => {
this.setState({ selectedAlmInstance: instance }, () => this.initialize());
};

render() { render() {
const { canAdmin, loadingBindings } = this.props;
const { canAdmin, loadingBindings, almInstances } = this.props;
const { const {
error, error,
importing, importing,
searchQuery, searchQuery,
selectedOrganization, selectedOrganization,
selectedRepository, selectedRepository,
selectedAlmInstance,
} = this.state; } = this.state;


return ( return (
repositories={repositories} repositories={repositories}
selectedOrganization={selectedOrganization} selectedOrganization={selectedOrganization}
selectedRepository={selectedRepository} selectedRepository={selectedRepository}
almInstances={almInstances}
selectedAlmInstance={selectedAlmInstance}
onSelectedAlmInstanceChange={this.onSelectedAlmInstanceChange}
/> />
); );
} }

+ 17
- 2
server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx View File

import { getBaseUrl } from '../../../helpers/system'; import { getBaseUrl } from '../../../helpers/system';
import { getProjectUrl } from '../../../helpers/urls'; import { getProjectUrl } from '../../../helpers/urls';
import { GithubOrganization, GithubRepository } from '../../../types/alm-integration'; import { GithubOrganization, GithubRepository } from '../../../types/alm-integration';
import { AlmSettingsInstance } from '../../../types/alm-settings';
import { ComponentQualifier } from '../../../types/component'; import { ComponentQualifier } from '../../../types/component';
import { Paging } from '../../../types/types'; import { Paging } from '../../../types/types';
import AlmSettingsInstanceDropdown from './AlmSettingsInstanceDropdown';
import CreateProjectPageHeader from './CreateProjectPageHeader'; import CreateProjectPageHeader from './CreateProjectPageHeader';


export interface GitHubProjectCreateRendererProps { export interface GitHubProjectCreateRendererProps {
searchQuery: string; searchQuery: string;
selectedOrganization?: GithubOrganization; selectedOrganization?: GithubOrganization;
selectedRepository?: GithubRepository; selectedRepository?: GithubRepository;
almInstances: AlmSettingsInstance[];
selectedAlmInstance?: AlmSettingsInstance;
onSelectedAlmInstanceChange: (instance: AlmSettingsInstance) => void;
} }


function orgToOption({ key, name }: GithubOrganization) { function orgToOption({ key, name }: GithubOrganization) {
organizations, organizations,
selectedOrganization, selectedOrganization,
selectedRepository, selectedRepository,
almInstances,
selectedAlmInstance,
} = props; } = props;


if (loadingBindings) { if (loadingBindings) {
} }
/> />


{error ? (
<AlmSettingsInstanceDropdown
almInstances={almInstances}
selectedAlmInstance={selectedAlmInstance}
onChangeConfig={props.onSelectedAlmInstanceChange}
/>

{error && selectedAlmInstance && (
<div className="display-flex-justify-center"> <div className="display-flex-justify-center">
<div className="boxed-group padded width-50 huge-spacer-top"> <div className="boxed-group padded width-50 huge-spacer-top">
<h2 className="big-spacer-bottom"> <h2 className="big-spacer-bottom">
</Alert> </Alert>
</div> </div>
</div> </div>
) : (
)}

{!error && (
<DeferredSpinner loading={loadingOrganizations}> <DeferredSpinner loading={loadingOrganizations}>
<div className="form-field"> <div className="form-field">
<label>{translate('onboarding.create_project.github.choose_organization')}</label> <label>{translate('onboarding.create_project.github.choose_organization')}</label>

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx View File

await this.fetchInitialData(); await this.fetchInitialData();
}; };


onChangeConfig = (instance: AlmSettingsInstance) => {
onSelectedAlmInstanceChange = (instance: AlmSettingsInstance) => {
this.setState({ this.setState({
selectedAlmInstance: instance, selectedAlmInstance: instance,
showPersonalAccessTokenForm: true, showPersonalAccessTokenForm: true,
showPersonalAccessTokenForm={ showPersonalAccessTokenForm={
showPersonalAccessTokenForm || Boolean(location.query.resetPat) showPersonalAccessTokenForm || Boolean(location.query.resetPat)
} }
onChangeConfig={this.onChangeConfig}
onSelectedAlmInstanceChange={this.onSelectedAlmInstanceChange}
/> />
); );
} }

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx View File

almInstances?: AlmSettingsInstance[]; almInstances?: AlmSettingsInstance[];
selectedAlmInstance?: AlmSettingsInstance; selectedAlmInstance?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean; showPersonalAccessTokenForm?: boolean;
onChangeConfig: (instance: AlmSettingsInstance) => void;
onSelectedAlmInstanceChange: (instance: AlmSettingsInstance) => void;
} }


export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) { export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) {
<AlmSettingsInstanceDropdown <AlmSettingsInstanceDropdown
almInstances={almInstances} almInstances={almInstances}
selectedAlmInstance={selectedAlmInstance} selectedAlmInstance={selectedAlmInstance}
onChangeConfig={props.onChangeConfig}
onChangeConfig={props.onSelectedAlmInstanceChange}
/> />


{loading && <i className="spinner" />} {loading && <i className="spinner" />}

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx View File

]} ]}
showPersonalAccessTokenForm={false} showPersonalAccessTokenForm={false}
submittingToken={false} submittingToken={false}
onChangeConfig={jest.fn()}
onSelectedAlmInstanceChange={jest.fn()}
{...overrides} {...overrides}
/> />
); );

+ 36
- 3
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProject-it.tsx View File

import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock'; import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock'; import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
import { renderApp } from '../../../../helpers/testReactTestingUtils'; import { renderApp } from '../../../../helpers/testReactTestingUtils';
import CreateProjectPage from '../CreateProjectPage';
import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';


jest.mock('../../../../api/alm-integrations'); jest.mock('../../../../api/alm-integrations');
jest.mock('../../../../api/alm-settings'); jest.mock('../../../../api/alm-settings');


const original = window.location;

let almIntegrationHandler: AlmIntegrationsServiceMock; let almIntegrationHandler: AlmIntegrationsServiceMock;
let almSettingsHandler: AlmSettingsServiceMock; let almSettingsHandler: AlmSettingsServiceMock;


const ui = { const ui = {
gitlabCreateProjectButton: byText('onboarding.create_project.select_method.gitlab'), gitlabCreateProjectButton: byText('onboarding.create_project.select_method.gitlab'),
githubCreateProjectButton: byText('onboarding.create_project.select_method.github'),
personalAccessTokenInput: byRole('textbox', { personalAccessTokenInput: byRole('textbox', {
name: 'onboarding.create_project.enter_pat field_required', name: 'onboarding.create_project.enter_pat field_required',
}), }),
}; };


beforeAll(() => { beforeAll(() => {
Object.defineProperty(window, 'location', {
configurable: true,
value: { replace: jest.fn() },
});
almIntegrationHandler = new AlmIntegrationsServiceMock(); almIntegrationHandler = new AlmIntegrationsServiceMock();
almSettingsHandler = new AlmSettingsServiceMock(); almSettingsHandler = new AlmSettingsServiceMock();
}); });
almSettingsHandler.reset(); almSettingsHandler.reset();
}); });


afterAll(() => {
Object.defineProperty(window, 'location', { configurable: true, value: original });
});

describe('Gitlab onboarding page', () => { describe('Gitlab onboarding page', () => {
it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => { it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
}); });
}); });


function renderCreateProject() {
renderApp('project/create', <CreateProjectPage />);
describe('Github onboarding page', () => {
it('should redirect to github authorization page when not already authorized', async () => {
const user = userEvent.setup();
renderCreateProject();
expect(ui.githubCreateProjectButton.get()).toBeInTheDocument();

await user.click(ui.githubCreateProjectButton.get());
expect(screen.getByText('onboarding.create_project.github.title')).toBeInTheDocument();
expect(screen.getByText('alm.configuration.selector.placeholder')).toBeInTheDocument();
expect(screen.getByText('alm.configuration.selector.label')).toBeInTheDocument();

await selectEvent.select(screen.getByLabelText('alm.configuration.selector.label'), [
/conf-github-1/,
]);

expect(window.location.replace).toHaveBeenCalled();
expect(
screen.getByText('onboarding.create_project.github.choose_organization')
).toBeInTheDocument();
});
});

function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
renderApp('project/create', <CreateProjectPage {...props} />);
} }

+ 4
- 4
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx View File

}); });


it('should handle no settings', async () => { it('should handle no settings', async () => {
const wrapper = shallowRender({ settings: [] });
const wrapper = shallowRender({ almInstances: [] });
await waitAndUpdate(wrapper); await waitAndUpdate(wrapper);
expect(wrapper.state().error).toBe(true); expect(wrapper.state().error).toBe(true);
}); });


it('should redirect when no code - github.com', async () => { it('should redirect when no code - github.com', async () => {
const wrapper = shallowRender({ const wrapper = shallowRender({
settings: [mockAlmSettingsInstance({ key: 'a', url: 'api.github.com' })],
almInstances: [mockAlmSettingsInstance({ key: 'a', url: 'api.github.com' })],
}); });
await waitAndUpdate(wrapper); await waitAndUpdate(wrapper);


expect(getGithubClientId).toHaveBeenCalled(); expect(getGithubClientId).toHaveBeenCalled();
expect(window.location.replace).toHaveBeenCalledWith( expect(window.location.replace).toHaveBeenCalledWith(
'github.com/login/oauth/authorize?client_id=client-id-124&redirect_uri=http://localhost/projects/create?mode=github'
'github.com/login/oauth/authorize?client_id=client-id-124&redirect_uri=http%3A%2F%2Flocalhost%2Fprojects%2Fcreate%3Fmode%3Dgithub%26almInstance%3Da'
); );
}); });


location={mockLocation()} location={mockLocation()}
onProjectCreate={jest.fn()} onProjectCreate={jest.fn()}
router={mockRouter()} router={mockRouter()}
settings={[mockAlmSettingsInstance({ key: 'a', url: 'geh.company.com/api/v3' })]}
almInstances={[mockAlmSettingsInstance({ key: 'a', url: 'geh.company.com/api/v3' })]}
{...props} {...props}
/> />
); );

+ 3
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx View File

import SearchBox from '../../../../components/controls/SearchBox'; import SearchBox from '../../../../components/controls/SearchBox';
import Select from '../../../../components/controls/Select'; import Select from '../../../../components/controls/Select';
import { mockGitHubRepository } from '../../../../helpers/mocks/alm-integrations'; import { mockGitHubRepository } from '../../../../helpers/mocks/alm-integrations';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { GithubOrganization } from '../../../../types/alm-integration'; import { GithubOrganization } from '../../../../types/alm-integration';
import GitHubProjectCreateRenderer, { import GitHubProjectCreateRenderer, {
GitHubProjectCreateRendererProps, GitHubProjectCreateRendererProps,
onSearch={jest.fn()} onSearch={jest.fn()}
onSelectOrganization={jest.fn()} onSelectOrganization={jest.fn()}
onSelectRepository={jest.fn()} onSelectRepository={jest.fn()}
onSelectedAlmInstanceChange={jest.fn()}
almInstances={[mockAlmSettingsInstance(), mockAlmSettingsInstance()]}
organizations={[]} organizations={[]}
repositoryPaging={{ total: 0, pageIndex: 1, pageSize: 30 }} repositoryPaging={{ total: 0, pageIndex: 1, pageSize: 30 }}
searchQuery="" searchQuery=""

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx View File

onLoadMore={jest.fn()} onLoadMore={jest.fn()}
onPersonalAccessTokenCreated={jest.fn()} onPersonalAccessTokenCreated={jest.fn()}
onSearch={jest.fn()} onSearch={jest.fn()}
onChangeConfig={jest.fn()}
onSelectedAlmInstanceChange={jest.fn()}
projects={undefined} projects={undefined}
projectsPaging={{ pageIndex: 1, pageSize: 30, total: 0 }} projectsPaging={{ pageIndex: 1, pageSize: 30, total: 0 }}
searching={false} searching={false}

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap View File

importing={false} importing={false}
loading={true} loading={true}
loadingRepositories={Object {}} loadingRepositories={Object {}}
onChangeConfig={[Function]}
onImportRepository={[Function]} onImportRepository={[Function]}
onOpenProject={[Function]} onOpenProject={[Function]}
onPersonalAccessTokenCreate={[Function]} onPersonalAccessTokenCreate={[Function]}
onSearch={[Function]} onSearch={[Function]}
onSelectRepository={[Function]} onSelectRepository={[Function]}
onSelectedAlmInstanceChange={[Function]}
repositories={Object {}} repositories={Object {}}
selectedAlmInstance={ selectedAlmInstance={
Object { Object {

+ 6
- 36
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap View File

className="display-flex-column" className="display-flex-column"
> >
<button <button
className="button button-huge display-flex-column create-project-mode-type-alm disabled big-spacer-right"
disabled={true}
className="button button-huge display-flex-column create-project-mode-type-alm big-spacer-right"
disabled={false}
onClick={[Function]} onClick={[Function]}
type="button" type="button"
> >
> >
onboarding.create_project.select_method.github onboarding.create_project.select_method.github
</div> </div>
<p
className="text-muted small spacer-top"
style={
Object {
"lineHeight": 1.5,
}
}
>
onboarding.create_project.too_many_alm_instances_X.alm.github
</p>
</button> </button>
</div> </div>
<div <div
className="display-flex-column" className="display-flex-column"
> >
<button <button
className="button button-huge display-flex-column create-project-mode-type-alm disabled big-spacer-right"
disabled={true}
className="button button-huge display-flex-column create-project-mode-type-alm big-spacer-right"
disabled={false}
onClick={[Function]} onClick={[Function]}
type="button" type="button"
> >
> >
onboarding.create_project.select_method.github onboarding.create_project.select_method.github
</div> </div>
<p
className="text-muted small spacer-top"
style={
Object {
"lineHeight": 1.5,
}
}
>
onboarding.create_project.too_many_alm_instances_X.alm.github
</p>
</button> </button>
</div> </div>
<div <div
className="display-flex-column" className="display-flex-column"
> >
<button <button
className="button button-huge display-flex-column create-project-mode-type-alm disabled big-spacer-right"
disabled={true}
className="button button-huge display-flex-column create-project-mode-type-alm big-spacer-right"
disabled={false}
onClick={[Function]} onClick={[Function]}
type="button" type="button"
> >
> >
onboarding.create_project.select_method.github onboarding.create_project.select_method.github
</div> </div>
<p
className="text-muted small spacer-top"
style={
Object {
"lineHeight": 1.5,
}
}
>
onboarding.create_project.too_many_alm_instances_X.alm.github
</p>
</button> </button>
</div> </div>
<div <div

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap View File

id="create-project" id="create-project"
> >
<GitHubProjectCreate <GitHubProjectCreate
almInstances={Array []}
canAdmin={false} canAdmin={false}
loadingBindings={true} loadingBindings={true}
location={ location={
"setRouteLeaveHook": [MockFunction], "setRouteLeaveHook": [MockFunction],
} }
} }
settings={Array []}
/> />
</div> </div>
</Fragment> </Fragment>

+ 90
- 48
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap View File

</span> </span>
} }
/> />
<AlmSettingsInstanceDropdown
almInstances={
Array [
Object {
"alm": "github",
"key": "key",
},
Object {
"alm": "github",
"key": "key",
},
]
}
onChangeConfig={[MockFunction]}
/>
<DeferredSpinner <DeferredSpinner
loading={false} loading={false}
> >
</span> </span>
} }
/> />
<div
className="display-flex-justify-center"
>
<div
className="boxed-group padded width-50 huge-spacer-top"
>
<h2
className="big-spacer-bottom"
>
onboarding.create_project.github.warning.title
</h2>
<Alert
variant="warning"
>
onboarding.create_project.github.warning.message
</Alert>
</div>
</div>
<AlmSettingsInstanceDropdown
almInstances={
Array [
Object {
"alm": "github",
"key": "key",
},
Object {
"alm": "github",
"key": "key",
},
]
}
onChangeConfig={[MockFunction]}
/>
</div> </div>
`; `;


</span> </span>
} }
/> />
<div
className="display-flex-justify-center"
>
<div
className="boxed-group padded width-50 huge-spacer-top"
>
<h2
className="big-spacer-bottom"
>
onboarding.create_project.github.warning.title
</h2>
<Alert
variant="warning"
>
<FormattedMessage
defaultMessage="onboarding.create_project.github.warning.message_admin"
id="onboarding.create_project.github.warning.message_admin"
values={
Object {
"link": <ForwardRef(Link)
to="/admin/settings?category=almintegration"
>
onboarding.create_project.github.warning.message_admin.link
</ForwardRef(Link)>,
}
}
/>
</Alert>
</div>
</div>
<AlmSettingsInstanceDropdown
almInstances={
Array [
Object {
"alm": "github",
"key": "key",
},
Object {
"alm": "github",
"key": "key",
},
]
}
onChangeConfig={[MockFunction]}
/>
</div> </div>
`; `;


</span> </span>
} }
/> />
<AlmSettingsInstanceDropdown
almInstances={
Array [
Object {
"alm": "github",
"key": "key",
},
Object {
"alm": "github",
"key": "key",
},
]
}
onChangeConfig={[MockFunction]}
/>
<DeferredSpinner <DeferredSpinner
loading={false} loading={false}
> >
</span> </span>
} }
/> />
<AlmSettingsInstanceDropdown
almInstances={
Array [
Object {
"alm": "github",
"key": "key",
},
Object {
"alm": "github",
"key": "key",
},
]
}
onChangeConfig={[MockFunction]}
/>
<DeferredSpinner <DeferredSpinner
loading={false} loading={false}
> >
</span> </span>
} }
/> />
<AlmSettingsInstanceDropdown
almInstances={
Array [
Object {
"alm": "github",
"key": "key",
},
Object {
"alm": "github",
"key": "key",
},
]
}
onChangeConfig={[MockFunction]}
/>
<DeferredSpinner <DeferredSpinner
loading={false} loading={false}
> >

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap View File

canAdmin={false} canAdmin={false}
loading={false} loading={false}
loadingMore={false} loadingMore={false}
onChangeConfig={[Function]}
onImport={[Function]} onImport={[Function]}
onLoadMore={[Function]} onLoadMore={[Function]}
onPersonalAccessTokenCreated={[Function]} onPersonalAccessTokenCreated={[Function]}
onSearch={[Function]} onSearch={[Function]}
onSelectedAlmInstanceChange={[Function]}
projectsPaging={ projectsPaging={
Object { Object {
"pageIndex": 1, "pageIndex": 1,

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/constants.ts View File



export const DEFAULT_BBS_PAGE_SIZE = 25; export const DEFAULT_BBS_PAGE_SIZE = 25;


export const ALLOWED_MULTIPLE_CONFIGS = [AlmKeys.GitLab, AlmKeys.Azure];
export const ALLOWED_MULTIPLE_CONFIGS = [AlmKeys.GitLab, AlmKeys.Azure, AlmKeys.GitHub];

+ 1
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx View File



wrapper = shallowRender(); wrapper = shallowRender();
await waitAndUpdate(wrapper); await waitAndUpdate(wrapper);
expect(wrapper.state().boundAlms).toEqual([AlmKeys.Azure, AlmKeys.GitLab]);
expect(wrapper.state().boundAlms).toEqual([AlmKeys.Azure, AlmKeys.GitHub, AlmKeys.GitLab]);
}); });


function shallowRender(overrides: Partial<ProjectCreationMenu['props']> = {}) { function shallowRender(overrides: Partial<ProjectCreationMenu['props']> = {}) {

+ 2
- 0
server/sonar-web/src/main/js/components/devops-platform/AlmSettingsInstanceSelector.tsx View File

*/ */
import * as React from 'react'; import * as React from 'react';
import { components, OptionProps, SingleValueProps } from 'react-select'; import { components, OptionProps, SingleValueProps } from 'react-select';
import { translate } from '../../helpers/l10n';
import { AlmSettingsInstance } from '../../types/alm-settings'; import { AlmSettingsInstance } from '../../types/alm-settings';
import Select from '../controls/Select'; import Select from '../controls/Select';


Option: optionRenderer, Option: optionRenderer,
SingleValue: singleValueRenderer, SingleValue: singleValueRenderer,
}} }}
placeholder={translate('alm.configuration.selector.placeholder')}
getOptionValue={(opt) => opt.key} getOptionValue={(opt) => opt.key}
value={instances.find((inst) => inst.key === initialValue)} value={instances.find((inst) => inst.key === initialValue)}
/> />

+ 3
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

alm.github.short=GitHub alm.github.short=GitHub
alm.gitlab=GitLab alm.gitlab=GitLab
alm.gitlab.short=GitLab alm.gitlab.short=GitLab
alm.configuration.selector.label=What DevOps platform do you want to import project from?
alm.configuration.selector.label=What DevOps platform configuration do you want to import projects from?
alm.configuration.selector.placeholder=Select a configuration


#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# #
onboarding.create_project.bitbucketcloud.title=Which Bitbucket Cloud repository do you want to set up? onboarding.create_project.bitbucketcloud.title=Which Bitbucket Cloud repository do you want to set up?
onboarding.create_project.bitbucketcloud.no_projects=No projects could be fetched from Bitbucket. Contact your system administrator, or {link}. onboarding.create_project.bitbucketcloud.no_projects=No projects could be fetched from Bitbucket. Contact your system administrator, or {link}.
onboarding.create_project.bitbucketcloud.link=See on Bitbucket onboarding.create_project.bitbucketcloud.link=See on Bitbucket
onboarding.create_project.github.title=Which GitHub repository do you want to set up?
onboarding.create_project.github.title=Github project onboarding
onboarding.create_project.github.choose_organization=Choose organization onboarding.create_project.github.choose_organization=Choose organization
onboarding.create_project.github.warning.title=Could not connect to GitHub onboarding.create_project.github.warning.title=Could not connect to GitHub
onboarding.create_project.github.warning.message=Please contact an administrator to configure GitHub integration. onboarding.create_project.github.warning.message=Please contact an administrator to configure GitHub integration.

Loading…
Cancel
Save