瀏覽代碼

SONAR-13475 - List Github Enterprise repositories API (#2883)

fixup! SONAR-13475 - List Github Enterprise repositories API (#2883)
tags/8.4.0.35506
Duarte Meneses 3 年之前
父節點
當前提交
db5fc17519
共有 18 個文件被更改,包括 502 次插入174 次删除
  1. 4
    3
      server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDao.java
  2. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingMapper.java
  3. 12
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml
  4. 28
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoTest.java
  5. 30
    11
      server/sonar-web/src/main/js/api/alm-integrations.ts
  6. 2
    11
      server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx
  7. 15
    10
      server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
  8. 103
    24
      server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx
  9. 105
    65
      server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx
  10. 2
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx
  11. 14
    24
      server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
  12. 74
    7
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx
  13. 8
    2
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx
  14. 1
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap
  15. 31
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
  16. 49
    7
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap
  17. 8
    8
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  18. 14
    0
      sonar-ws/src/main/protobuf/ws-alm_integrations.proto

+ 4
- 3
server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDao.java 查看文件

@@ -22,13 +22,10 @@ package org.sonar.db.alm.setting;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.sonar.api.utils.System2;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.Dao;
import org.sonar.db.DbSession;
import org.sonar.db.alm.AlmAppInstallDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.project.ProjectDto;

import static org.sonar.db.DatabaseUtils.executeLargeInputs;
@@ -83,4 +80,8 @@ public class ProjectAlmSettingDao implements Dao {
public List<ProjectAlmSettingDto> selectByAlmSettingAndSlugs(DbSession dbSession, AlmSettingDto almSettingDto, Set<String> almSlugs) {
return executeLargeInputs(almSlugs, slugs -> getMapper(dbSession).selectByAlmSettingAndSlugs(almSettingDto.getUuid(), slugs));
}

public List<ProjectAlmSettingDto> selectByAlmSettingAndRepos(DbSession dbSession, AlmSettingDto almSettingDto, Set<String> almRepos) {
return executeLargeInputs(almRepos, repos -> getMapper(dbSession).selectByAlmSettingAndRepos(almSettingDto.getUuid(), repos));
}
}

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingMapper.java 查看文件

@@ -39,4 +39,6 @@ public interface ProjectAlmSettingMapper {
void deleteByAlmSettingUuid(@Param("almSettingUuid") String almSettingUuid);

List<ProjectAlmSettingDto> selectByAlmSettingAndSlugs(@Param("almSettingUuid") String almSettingUuid, @Param("slugs") List<String> slugs);

List<ProjectAlmSettingDto> selectByAlmSettingAndRepos(@Param("almSettingUuid") String almSettingUuid, @Param("repos") List<String> repos);
}

+ 12
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml 查看文件

@@ -34,6 +34,18 @@
</foreach>
</select>

<select id="selectByAlmSettingAndRepos" parameterType="string" resultType="org.sonar.db.alm.setting.ProjectAlmSettingDto">
select <include refid="sqlColumns"/>
from
project_alm_settings p
where
alm_setting_uuid=#{almSettingUuid, jdbcType=VARCHAR}
and alm_repo in
<foreach collection="repos" open="(" close=")" item="repo" separator=",">
#{repo, jdbcType=VARCHAR}
</foreach>
</select>

<insert id="insert" parameterType="Map" useGeneratedKeys="false">
INSERT INTO project_alm_settings
(

+ 28
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoTest.java 查看文件

@@ -99,6 +99,34 @@ public class ProjectAlmSettingDaoTest {
assertThat(underTest.selectByAlmSettingAndSlugs(dbSession, almSettingsDto, new HashSet<>())).isEmpty();
}

@Test
public void select_by_alm_setting_and_repos() {
when(uuidFactory.create()).thenReturn(A_UUID);
AlmSettingDto almSettingsDto = db.almSettings().insertGitHubAlmSetting();
ProjectDto project = db.components().insertPrivateProjectDto();
ProjectAlmSettingDto githubProjectAlmSettingDto = newGithubProjectAlmSettingDto(almSettingsDto, project);
githubProjectAlmSettingDto.setAlmRepo("repo1");
underTest.insertOrUpdate(dbSession, githubProjectAlmSettingDto);
ProjectAlmSettingDto githubProjectAlmSettingDto2 = newGithubProjectAlmSettingDto(almSettingsDto, db.components().insertPrivateProjectDto());
githubProjectAlmSettingDto2.setAlmRepo("repo2");
when(uuidFactory.create()).thenReturn(A_UUID + 1);
underTest.insertOrUpdate(dbSession, githubProjectAlmSettingDto2);

Set<String> repos = new HashSet<>();
repos.add("repo1");
assertThat(underTest.selectByAlmSettingAndRepos(dbSession, almSettingsDto, repos))
.extracting(ProjectAlmSettingDto::getProjectUuid, ProjectAlmSettingDto::getSummaryCommentEnabled)
.containsExactly(tuple(project.getUuid(), githubProjectAlmSettingDto.getSummaryCommentEnabled()));
}

@Test
public void select_with_no_repos_return_empty() {
when(uuidFactory.create()).thenReturn(A_UUID);
AlmSettingDto almSettingsDto = db.almSettings().insertGitHubAlmSetting();

assertThat(underTest.selectByAlmSettingAndRepos(dbSession, almSettingsDto, new HashSet<>())).isEmpty();
}

@Test
public void update_existing_binding() {
when(uuidFactory.create()).thenReturn(A_UUID);

+ 30
- 11
server/sonar-web/src/main/js/api/alm-integrations.ts 查看文件

@@ -87,30 +87,49 @@ export function searchForBitbucketServerRepositories(
});
}

export function getGithubClientId(almSetting: string): Promise<{ clientId: string }> {
export function getGithubClientId(almSetting: string): Promise<{ clientId?: string }> {
return getJSON('/api/alm_integrations/get_github_client_id', { almSetting });
}

export function importGithubRepository(
almSetting: string,
organization: string,
repositoryKey: string
): Promise<{ project: ProjectBase }> {
return postJSON('/api/alm_integrations/import_github_project', {
almSetting,
organization,
repositoryKey
}).catch(throwGlobalError);
}

export function getGithubOrganizations(
almSetting: string,
token: string
): Promise<{ organizations: GithubOrganization[] }> {
return getJSON('/api/alm_integrations/list_github_enterprise_organizations', {
return getJSON('/api/alm_integrations/list_github_organizations', {
almSetting,
token
}).catch((response?: Response) => {
if (response && response.status !== 400) {
throwGlobalError(response);
}
});
}

export function getGithubRepositories(
almSetting: string,
organization: string,
p = 1,
query?: string
): Promise<{ repositories: GithubRepository[]; paging: T.Paging }> {
return getJSON('/api/alm_integrations/list_github_enterprise_repositories', {
export function getGithubRepositories(data: {
almSetting: string;
organization: string;
ps: number;
p?: number;
query?: string;
}): Promise<{ repositories: GithubRepository[]; paging: T.Paging }> {
const { almSetting, organization, ps, p = 1, query } = data;
return getJSON('/api/alm_integrations/list_github_repositories', {
almSetting,
organization,
p,
query: query || undefined
});
ps,
q: query || undefined
}).catch(throwGlobalError);
}

+ 2
- 11
server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx 查看文件

@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { connect } from 'react-redux';
import { WithRouterProps } from 'react-router';
import {
checkPersonalAccessTokenIsValid,
@@ -28,7 +27,6 @@ import {
searchForBitbucketServerRepositories,
setAlmPersonalAccessToken
} from '../../../api/alm-integrations';
import { getAppState, Store } from '../../../store/rootReducer';
import {
BitbucketProject,
BitbucketProjectRepositories,
@@ -38,8 +36,8 @@ import { AlmSettingsInstance } from '../../../types/alm-settings';
import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer';

interface Props extends Pick<WithRouterProps, 'location'> {
canAdmin: boolean;
bitbucketSettings: AlmSettingsInstance[];
canAdmin?: boolean;
loadingBindings: boolean;
onProjectCreate: (projectKeys: string[]) => void;
}
@@ -58,7 +56,7 @@ interface State {
tokenValidationFailed: boolean;
}

export class BitbucketProjectCreate extends React.PureComponent<Props, State> {
export default class BitbucketProjectCreate extends React.PureComponent<Props, State> {
mounted = false;

constructor(props: Props) {
@@ -285,10 +283,3 @@ export class BitbucketProjectCreate extends React.PureComponent<Props, State> {
);
}
}

const mapStateToProps = (state: Store) => {
const { canAdmin } = getAppState(state);
return { canAdmin };
};

export default connect(mapStateToProps)(BitbucketProjectCreate);

+ 15
- 10
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx 查看文件

@@ -47,7 +47,7 @@ interface State {

export class CreateProjectPage extends React.PureComponent<Props, State> {
mounted = false;
state: State = { bitbucketSettings: [], githubSettings: [], loading: false };
state: State = { bitbucketSettings: [], githubSettings: [], loading: true };

componentDidMount() {
const {
@@ -82,12 +82,6 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
});
};

handleProjectCreate = (projectKeys: string[]) => {
if (projectKeys.length === 1) {
this.props.router.push(getProjectUrl(projectKeys[0]));
}
};

handleModeSelect = (mode: CreateProjectModes) => {
const { router, location } = this.props;
router.push({
@@ -96,10 +90,17 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
});
};

handleProjectCreate = (projectKeys: string[]) => {
if (projectKeys.length === 1) {
this.props.router.push(getProjectUrl(projectKeys[0]));
}
};

renderForm(mode?: CreateProjectModes) {
const {
appState: { canAdmin },
location
location,
router
} = this.props;
const { bitbucketSettings, githubSettings, loading } = this.state;

@@ -107,6 +108,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
case CreateProjectModes.BitbucketServer: {
return (
<BitbucketProjectCreate
canAdmin={!!canAdmin}
bitbucketSettings={bitbucketSettings}
loadingBindings={loading}
location={location}
@@ -118,8 +120,11 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
return (
<GitHubProjectCreate
canAdmin={!!canAdmin}
code={location.query?.code}
settings={githubSettings[0]}
loadingBindings={loading}
location={location}
onProjectCreate={this.handleProjectCreate}
router={router}
settings={githubSettings}
/>
);
}

+ 103
- 24
server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx 查看文件

@@ -19,25 +19,29 @@
*/
import { debounce } from 'lodash';
import * as React from 'react';
import { WithRouterProps } from 'react-router';
import { getHostUrl } from 'sonar-ui-common/helpers/urls';
import {
getGithubClientId,
getGithubOrganizations,
getGithubRepositories
getGithubRepositories,
importGithubRepository
} from '../../../api/alm-integrations';
import { GithubOrganization, GithubRepository } from '../../../types/alm-integration';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';

interface Props {
interface Props extends Pick<WithRouterProps, 'location' | 'router'> {
canAdmin: boolean;
code?: string;
settings?: AlmSettingsInstance;
loadingBindings: boolean;
onProjectCreate: (projectKeys: string[]) => void;
settings: AlmSettingsInstance[];
}

interface State {
error: boolean;
loading: boolean;
importing: boolean;
loadingOrganizations: boolean;
loadingRepositories: boolean;
organizations: GithubOrganization[];
repositoryPaging: T.Paging;
@@ -45,6 +49,7 @@ interface State {
searchQuery: string;
selectedOrganization?: GithubOrganization;
selectedRepository?: GithubRepository;
settings?: AlmSettingsInstance;
}

const REPOSITORY_PAGE_SIZE = 30;
@@ -57,12 +62,14 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {

this.state = {
error: false,
loading: true,
importing: false,
loadingOrganizations: true,
loadingRepositories: false,
organizations: [],
repositories: [],
repositoryPaging: { pageSize: REPOSITORY_PAGE_SIZE, total: 0, pageIndex: 1 },
searchQuery: ''
searchQuery: '',
settings: props.settings[0]
};

this.triggerSearch = debounce(this.triggerSearch, 250);
@@ -75,8 +82,8 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
}

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

@@ -85,19 +92,24 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
}

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

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

const code = location.query?.code;

try {
if (!code) {
await this.redirectToGithub(settings);
} else {
delete location.query.code;
router.replace(location);
await this.fetchOrganizations(settings, code);
}
} catch (e) {
@@ -108,8 +120,17 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
}

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

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

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

const queryParams = [
{ param: 'client_id', value: clientId },
{ param: 'redirect_uri', value: `${getHostUrl()}/projects/create?mode=${AlmKeys.GitHub}` }
@@ -117,20 +138,32 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
.map(({ param, value }) => `${param}=${value}`)
.join('&');

window.location.replace(`https://github.com/login/oauth/authorize?${queryParams}`);
let instanceRootUrl;
// Strip the api section from the url, since we're not hitting the api here.
if (settings.url.includes('/api/v3')) {
// GitHub Enterprise
instanceRootUrl = settings.url.replace('/api/v3', '');
} else {
// github.com
instanceRootUrl = settings.url.replace('api.', '');
}

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

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

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

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

if (!settings) {
this.setState({ error: true });
@@ -139,26 +172,45 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {

this.setState({ loadingRepositories: true });

const data = await getGithubRepositories(settings.key, organizationKey, page, query);
try {
const data = await getGithubRepositories({
almSetting: settings.key,
organization: organizationKey,
ps: REPOSITORY_PAGE_SIZE,
p: page,
query
});

if (this.mounted) {
this.setState(({ repositories }) => ({
loadingRepositories: false,
repositoryPaging: data.paging,
repositories: page === 1 ? data.repositories : [...repositories, ...data.repositories]
}));
if (this.mounted) {
this.setState(({ repositories }) => ({
loadingRepositories: false,
repositoryPaging: data.paging,
repositories: page === 1 ? data.repositories : [...repositories, ...data.repositories]
}));
}
} catch (_) {
if (this.mounted) {
this.setState({
loadingRepositories: false,
repositoryPaging: { pageIndex: 1, pageSize: REPOSITORY_PAGE_SIZE, total: 0 },
repositories: []
});
}
}
}

triggerSearch = (query: string) => {
const { selectedOrganization } = this.state;
if (selectedOrganization) {
this.setState({ selectedRepository: undefined });
this.fetchRepositories({ organizationKey: selectedOrganization.key, query });
}
};

handleSelectOrganization = (key: string) => {
this.setState(({ organizations }) => ({
searchQuery: '',
selectedRepository: undefined,
selectedOrganization: organizations.find(o => o.key === key)
}));
this.fetchRepositories({ organizationKey: key });
@@ -187,11 +239,34 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
}
};

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

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

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

this.props.onProjectCreate([project.key]);
} finally {
if (this.mounted) {
this.setState({ importing: false });
}
}
}
};

render() {
const { canAdmin } = this.props;
const { canAdmin, loadingBindings } = this.props;
const {
error,
loading,
importing,
loadingOrganizations,
loadingRepositories,
organizations,
repositoryPaging,
@@ -200,12 +275,16 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
selectedOrganization,
selectedRepository
} = this.state;

return (
<GitHubProjectCreateRenderer
canAdmin={canAdmin}
error={error}
loading={loading}
importing={importing}
loadingBindings={loadingBindings}
loadingOrganizations={loadingOrganizations}
loadingRepositories={loadingRepositories}
onImportRepository={this.handleImportRepository}
onLoadMore={this.handleLoadMore}
onSearch={this.handleSearch}
onSelectOrganization={this.handleSelectOrganization}

+ 105
- 65
server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx 查看文件

@@ -20,6 +20,7 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
import Radio from 'sonar-ui-common/components/controls/Radio';
import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
@@ -35,8 +36,11 @@ import CreateProjectPageHeader from './CreateProjectPageHeader';
export interface GitHubProjectCreateRendererProps {
canAdmin: boolean;
error: boolean;
loading: boolean;
importing: boolean;
loadingBindings: boolean;
loadingOrganizations: boolean;
loadingRepositories: boolean;
onImportRepository: () => void;
onLoadMore: () => void;
onSearch: (q: string) => void;
onSelectOrganization: (key: string) => void;
@@ -53,13 +57,13 @@ function orgToOption({ key, name }: GithubOrganization) {
return { value: key, label: name };
}

export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRendererProps) {
const handleSearch = (organizations: GithubOrganization[]) => (q: string) =>
Promise.resolve(organizations.filter(o => !q || o.name.includes(q)).map(orgToOption));

function renderRepositoryList(props: GitHubProjectCreateRendererProps) {
const {
canAdmin,
error,
loading,
importing,
loadingRepositories,
organizations,
repositories,
repositoryPaging,
searchQuery,
@@ -67,9 +71,99 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
selectedRepository
} = props;

const isChecked = (repository: GithubRepository) =>
!!repository.sqProjectKey ||
(!!selectedRepository && selectedRepository.key === repository.key);

const isDisabled = (repository: GithubRepository) =>
!!repository.sqProjectKey || loadingRepositories || importing;

return (
selectedOrganization &&
repositories && (
<div className="boxed-group padded display-flex-wrap">
<div className="width-100">
<SearchBox
className="big-spacer-bottom"
onChange={props.onSearch}
placeholder={translate('onboarding.create_project.search_repositories')}
value={searchQuery}
/>
</div>

{repositories.length === 0 ? (
<div className="padded">
<DeferredSpinner loading={loadingRepositories}>
{translate('no_results')}
</DeferredSpinner>
</div>
) : (
repositories.map(r => (
<Radio
className="spacer-top spacer-bottom padded create-project-github-repository"
key={r.key}
checked={isChecked(r)}
disabled={isDisabled(r)}
value={r.key}
onCheck={props.onSelectRepository}>
<div className="big overflow-hidden" title={r.name}>
<div className="text-ellipsis">{r.name}</div>
{r.sqProjectKey && (
<em className="notice text-muted-2 small display-flex-center">
{translate('onboarding.create_project.repository_imported')}
<CheckIcon className="little-spacer-left" size={12} />
</em>
)}
</div>
</Radio>
))
)}

<div className="display-flex-justify-center width-100">
<ListFooter
count={repositories.length}
total={repositoryPaging.total}
loadMore={props.onLoadMore}
loading={loadingRepositories}
/>
</div>
</div>
)
);
}

export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRendererProps) {
const {
canAdmin,
error,
importing,
loadingBindings,
loadingOrganizations,
organizations,
selectedOrganization,
selectedRepository
} = props;

if (loadingBindings) {
return <DeferredSpinner />;
}

return (
<div>
<CreateProjectPageHeader
additionalActions={
selectedOrganization && (
<div className="display-flex-center pull-right">
<DeferredSpinner className="spacer-right" loading={importing} />
<Button
className="button-large button-primary"
disabled={!selectedRepository || importing}
onClick={props.onImportRepository}>
{translate('onboarding.create_project.import_selected_repo')}
</Button>
</div>
)
}
title={
<span className="text-middle display-flex-center">
<img
@@ -111,23 +205,19 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
</div>
</div>
) : (
<DeferredSpinner loading={loading}>
<DeferredSpinner loading={loadingOrganizations}>
<div className="form-field">
<label>{translate('onboarding.create_project.github.choose_organization')}</label>
{organizations.length > 0 ? (
<SearchSelect
defaultOptions={organizations.slice(0, 10).map(orgToOption)}
onSearch={(q: string) =>
Promise.resolve(
organizations.filter(o => !q || o.name.includes(q)).map(orgToOption)
)
}
defaultOptions={organizations.map(orgToOption)}
onSearch={handleSearch(organizations)}
minimumQueryLength={0}
onSelect={({ value }) => props.onSelectOrganization(value)}
value={selectedOrganization && orgToOption(selectedOrganization)}
/>
) : (
!loading && (
!loadingOrganizations && (
<Alert className="spacer-top" variant="error">
{canAdmin ? (
<FormattedMessage
@@ -153,57 +243,7 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
</DeferredSpinner>
)}

{selectedOrganization && repositories && (
<div className="boxed-group padded display-flex-wrap">
<div className="width-100">
<SearchBox
className="big-spacer-bottom"
onChange={props.onSearch}
placeholder={translate('onboarding.create_project.search_repositories')}
value={searchQuery}
/>
</div>

{repositories.length === 0 ? (
<div className="padded">
<DeferredSpinner loading={loadingRepositories}>
{translate('no_results')}
</DeferredSpinner>
</div>
) : (
repositories.map(r => (
<Radio
className="spacer-top spacer-bottom padded create-project-github-repository"
key={r.key}
checked={
!!r.sqProjectKey || (!!selectedRepository && selectedRepository.key === r.key)
}
disabled={!!r.sqProjectKey || loadingRepositories || importing}
value={r.key}
onCheck={props.onSelectRepository}>
<div className="big overflow-hidden" title={r.name}>
<div className="overflow-hidden text-ellipsis">{r.name}</div>
{r.sqProjectKey && (
<em className="notice text-muted-2 small display-flex-center">
{translate('onboarding.create_project.repository_imported')}
<CheckIcon className="little-spacer-left" size={12} />
</em>
)}
</div>
</Radio>
))
)}

<div className="display-flex-justify-center width-100">
<ListFooter
count={repositories.length}
total={repositoryPaging.total}
loadMore={props.onLoadMore}
loading={loadingRepositories}
/>
</div>
</div>
)}
{renderRepositoryList(props)}
</div>
);
}

+ 2
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx 查看文件

@@ -33,7 +33,7 @@ import { mockBitbucketRepository } from '../../../../helpers/mocks/alm-integrati
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockLocation } from '../../../../helpers/testMocks';
import { AlmKeys } from '../../../../types/alm-settings';
import { BitbucketProjectCreate } from '../BitbucketProjectCreate';
import BitbucketProjectCreate from '../BitbucketProjectCreate';

jest.mock('../../../../api/alm-integrations', () => {
const { mockBitbucketProject, mockBitbucketRepository } = jest.requireActual(
@@ -163,6 +163,7 @@ it('should correctly handle search', async () => {
function shallowRender(props: Partial<BitbucketProjectCreate['props']> = {}) {
return shallow<BitbucketProjectCreate>(
<BitbucketProjectCreate
canAdmin={false}
bitbucketSettings={[mockAlmSettingsInstance({ alm: AlmKeys.Bitbucket, key: 'foo' })]}
loadingBindings={false}
location={mockLocation()}

+ 14
- 24
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx 查看文件

@@ -43,36 +43,26 @@ it('should render correctly if no branch support', () => {
});

it('should render correctly if the manual method is selected', () => {
const push = jest.fn();
const location = { query: { mode: CreateProjectModes.Manual } };
const wrapper = shallowRender({ router: mockRouter({ push }) });

wrapper.instance().handleModeSelect(CreateProjectModes.Manual);
expect(push).toBeCalledWith(expect.objectContaining(location));

expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot();
expect(
shallowRender({
location: mockLocation({ query: { mode: CreateProjectModes.Manual } })
})
).toMatchSnapshot();
});

it('should render correctly if the BBS method is selected', () => {
const push = jest.fn();
const location = { query: { mode: CreateProjectModes.BitbucketServer } };
const wrapper = shallowRender({ router: mockRouter({ push }) });

wrapper.instance().handleModeSelect(CreateProjectModes.BitbucketServer);
expect(push).toBeCalledWith(expect.objectContaining(location));

expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot();
expect(
shallowRender({
location: mockLocation({ query: { mode: CreateProjectModes.BitbucketServer } })
})
).toMatchSnapshot();
});

it('should render correctly if the GitHub method is selected', () => {
const push = jest.fn();
const location = { query: { mode: CreateProjectModes.GitHub } };
const wrapper = shallowRender({ router: mockRouter({ push }) });

wrapper.instance().handleModeSelect(CreateProjectModes.GitHub);
expect(push).toBeCalledWith(expect.objectContaining(location));

expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot();
const wrapper = shallowRender({
location: mockLocation({ query: { mode: CreateProjectModes.GitHub } })
});
expect(wrapper).toMatchSnapshot();
});

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

+ 74
- 7
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx 查看文件

@@ -24,16 +24,19 @@ import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import {
getGithubClientId,
getGithubOrganizations,
getGithubRepositories
getGithubRepositories,
importGithubRepository
} from '../../../../api/alm-integrations';
import { mockGitHubRepository } from '../../../../helpers/mocks/alm-integrations';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
import GitHubProjectCreate from '../GitHubProjectCreate';

jest.mock('../../../../api/alm-integrations', () => ({
getGithubClientId: jest.fn().mockResolvedValue({ clientId: 'client-id-124' }),
getGithubOrganizations: jest.fn().mockResolvedValue({ organizations: [] }),
getGithubRepositories: jest.fn().mockResolvedValue({ repositories: [], paging: {} })
getGithubRepositories: jest.fn().mockResolvedValue({ repositories: [], paging: {} }),
importGithubRepository: jest.fn().mockResolvedValue({ project: {} })
}));

const originalLocation = window.location;
@@ -61,7 +64,7 @@ beforeEach(() => {
});

it('should handle no settings', async () => {
const wrapper = shallowRender({ settings: undefined });
const wrapper = shallowRender({ settings: [] });
await waitAndUpdate(wrapper);
expect(wrapper.state().error).toBe(true);
});
@@ -74,15 +77,41 @@ it('should redirect when no code', async () => {
expect(window.location.replace).toBeCalled();
});

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

expect(getGithubClientId).toBeCalled();
expect(window.location.replace).toBeCalledWith(
'github.com/login/oauth/authorize?client_id=client-id-124&redirect_uri=http://localhost/projects/create?mode=github'
);
});

it('should not redirect when invalid clientId', async () => {
(getGithubClientId as jest.Mock).mockResolvedValue({ clientId: undefined });
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

expect(wrapper.state().error).toBe(true);
expect(window.location.replace).not.toBeCalled();
});

it('should fetch organizations when code', async () => {
const organizations = [
{ key: '1', name: 'org1' },
{ key: '2', name: 'org2' }
];
(getGithubOrganizations as jest.Mock).mockResolvedValueOnce({ organizations });
const wrapper = shallowRender({ code: '123456' });
const replace = jest.fn();
const wrapper = shallowRender({
location: mockLocation({ query: { code: '123456' } }),
router: mockRouter({ replace })
});
await waitAndUpdate(wrapper);

expect(replace).toBeCalled();
expect(getGithubOrganizations).toBeCalled();
expect(wrapper.state().organizations).toBe(organizations);
});
@@ -98,7 +127,7 @@ it('should handle org selection', async () => {
repositories,
paging: { total: 1, pageIndex: 1 }
});
const wrapper = shallowRender({ code: '123456' });
const wrapper = shallowRender({ location: mockLocation({ query: { code: '123456' } }) });
await waitAndUpdate(wrapper);

wrapper.instance().handleSelectOrganization('1');
@@ -154,7 +183,13 @@ it('should handle search', async () => {

await waitAndUpdate(wrapper);

expect(getGithubRepositories).toBeCalledWith('a', 'o1', 1, query);
expect(getGithubRepositories).toBeCalledWith({
almSetting: 'a',
organization: 'o1',
p: 1,
ps: 30,
query: 'query'
});
expect(wrapper.state().repositories).toEqual(repositories);
});

@@ -169,11 +204,43 @@ it('should handle repository selection', async () => {
expect(wrapper.state().selectedRepository).toBe(repo);
});

it('should handle importing', async () => {
const project = { key: 'new_project' };

(importGithubRepository as jest.Mock).mockResolvedValueOnce({ project });

const onProjectCreate = jest.fn();
const wrapper = shallowRender({ onProjectCreate });

wrapper.instance().handleImportRepository();
expect(importGithubRepository).not.toBeCalled();

const selectedOrganization = { key: 'org1', name: 'org1' };
const selectedRepository = mockGitHubRepository();
wrapper.setState({
selectedOrganization,
selectedRepository
});

wrapper.instance().handleImportRepository();
await waitAndUpdate(wrapper);
expect(importGithubRepository).toBeCalledWith(
'a',
selectedOrganization.key,
selectedRepository.key
);
expect(onProjectCreate).toBeCalledWith([project.key]);
});

function shallowRender(props: Partial<GitHubProjectCreate['props']> = {}) {
return shallow<GitHubProjectCreate>(
<GitHubProjectCreate
canAdmin={false}
settings={mockAlmSettingsInstance({ key: 'a' })}
loadingBindings={false}
location={mockLocation()}
onProjectCreate={jest.fn()}
router={mockRouter()}
settings={[mockAlmSettingsInstance({ key: 'a', url: 'geh.company.com/api/v3' })]}
{...props}
/>
);

+ 8
- 2
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx 查看文件

@@ -31,6 +31,7 @@ import GitHubProjectCreateRenderer, {

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ loadingBindings: true })).toMatchSnapshot('loading');
expect(shallowRender({ error: true })).toMatchSnapshot('error');
expect(shallowRender({ canAdmin: true, error: true })).toMatchSnapshot('error for admin');

@@ -64,11 +65,13 @@ it('should render correctly', () => {
});

describe('callback', () => {
const onImportRepository = jest.fn();
const onSelectOrganization = jest.fn();
const onSelectRepository = jest.fn();
const onSearch = jest.fn();
const org = { key: 'o1', name: 'org' };
const wrapper = shallowRender({
onImportRepository,
onSelectOrganization,
onSelectRepository,
onSearch,
@@ -83,7 +86,7 @@ describe('callback', () => {

it('should be called when org is selected', () => {
const value = 'o1';
wrapper.find(SearchSelect).props().onSelect!({ value });
wrapper.find(SearchSelect).simulate('select', { value });
expect(onSelectOrganization).toBeCalledWith(value);
});

@@ -111,8 +114,11 @@ function shallowRender(props: Partial<GitHubProjectCreateRendererProps> = {}) {
<GitHubProjectCreateRenderer
canAdmin={false}
error={false}
loading={false}
importing={false}
loadingBindings={false}
loadingOrganizations={false}
loadingRepositories={false}
onImportRepository={jest.fn()}
onLoadMore={jest.fn()}
onSearch={jest.fn()}
onSelectOrganization={jest.fn()}

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap 查看文件

@@ -8,6 +8,7 @@ exports[`should render correctly 1`] = `
"key": "foo",
}
}
canAdmin={false}
importing={false}
loading={true}
onImportRepository={[Function]}

+ 31
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap 查看文件

@@ -68,8 +68,9 @@ exports[`should render correctly if the BBS method is selected 1`] = `
className="page page-limited huge-spacer-bottom position-relative"
id="create-project"
>
<Connect(BitbucketProjectCreate)
<BitbucketProjectCreate
bitbucketSettings={Array []}
canAdmin={false}
loadingBindings={true}
location={
Object {
@@ -107,6 +108,35 @@ exports[`should render correctly if the GitHub method is selected 1`] = `
>
<GitHubProjectCreate
canAdmin={false}
loadingBindings={true}
location={
Object {
"action": "PUSH",
"hash": "",
"key": "key",
"pathname": "/path",
"query": Object {
"mode": "github",
},
"search": "",
"state": Object {},
}
}
onProjectCreate={[Function]}
router={
Object {
"createHref": [MockFunction],
"createPath": [MockFunction],
"go": [MockFunction],
"goBack": [MockFunction],
"goForward": [MockFunction],
"isActive": [MockFunction],
"push": [MockFunction],
"replace": [MockFunction],
"setRouteLeaveHook": [MockFunction],
}
}
settings={Array []}
/>
</div>
</Fragment>

+ 49
- 7
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap 查看文件

@@ -128,9 +128,33 @@ exports[`should render correctly: error for admin 1`] = `
</div>
`;

exports[`should render correctly: loading 1`] = `
<DeferredSpinner
timeout={100}
/>
`;

exports[`should render correctly: no repositories 1`] = `
<div>
<CreateProjectPageHeader
additionalActions={
<div
className="display-flex-center pull-right"
>
<DeferredSpinner
className="spacer-right"
loading={false}
timeout={100}
/>
<Button
className="button-large button-primary"
disabled={true}
onClick={[MockFunction]}
>
onboarding.create_project.import_selected_repo
</Button>
</div>
}
title={
<span
className="text-middle display-flex-center"
@@ -235,6 +259,24 @@ exports[`should render correctly: organizations 1`] = `
exports[`should render correctly: repositories 1`] = `
<div>
<CreateProjectPageHeader
additionalActions={
<div
className="display-flex-center pull-right"
>
<DeferredSpinner
className="spacer-right"
loading={false}
timeout={100}
/>
<Button
className="button-large button-primary"
disabled={false}
onClick={[MockFunction]}
>
onboarding.create_project.import_selected_repo
</Button>
</div>
}
title={
<span
className="text-middle display-flex-center"
@@ -299,7 +341,7 @@ exports[`should render correctly: repositories 1`] = `
</div>
<Radio
checked={false}
className="spacer-top spacer-bottom padded github-repository"
className="spacer-top spacer-bottom padded create-project-github-repository"
disabled={false}
key="repo1"
onCheck={[MockFunction]}
@@ -310,7 +352,7 @@ exports[`should render correctly: repositories 1`] = `
title="repository 1"
>
<div
className="overflow-hidden text-ellipsis"
className="text-ellipsis"
>
repository 1
</div>
@@ -318,7 +360,7 @@ exports[`should render correctly: repositories 1`] = `
</Radio>
<Radio
checked={true}
className="spacer-top spacer-bottom padded github-repository"
className="spacer-top spacer-bottom padded create-project-github-repository"
disabled={true}
key="repo2"
onCheck={[MockFunction]}
@@ -329,14 +371,14 @@ exports[`should render correctly: repositories 1`] = `
title="repository 1"
>
<div
className="overflow-hidden text-ellipsis"
className="text-ellipsis"
>
repository 1
</div>
<em
className="notice text-muted-2 small display-flex-center"
>
onboarding.create_project.already_imported
onboarding.create_project.repository_imported
<CheckIcon
className="little-spacer-left"
size={12}
@@ -346,7 +388,7 @@ exports[`should render correctly: repositories 1`] = `
</Radio>
<Radio
checked={true}
className="spacer-top spacer-bottom padded github-repository"
className="spacer-top spacer-bottom padded create-project-github-repository"
disabled={false}
key="repo3"
onCheck={[MockFunction]}
@@ -357,7 +399,7 @@ exports[`should render correctly: repositories 1`] = `
title="repository 1"
>
<div
className="overflow-hidden text-ellipsis"
className="text-ellipsis"
>
repository 1
</div>

+ 8
- 8
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -1080,8 +1080,8 @@ settings.almintegration.feature.mr_decoration.description=Add analysis and a Qua
settings.almintegration.feature.alm_repo_import.title=Import repositories from your ALM
settings.almintegration.feature.alm_repo_import.description=Select repositories from your ALM, and import them into SonarQube.
settings.almintegration.feature.alm_repo_import.disabled_if_multiple_bbs_instances=Connecting to multiple Bitbucket Server instances will deactivate the {feature} feature. Projects will have to be set up manually.
settings.almintegration.feature.alm_repo_import.disabled_if_multiple_github_instances=Connecting to multiple GitHub Enterprise instances will deactivate the {feature} feature. Projects will have to be set up manually.
settings.almintegration.feature.alm_repo_import.github.too_many_instances_x=You must have exactly 1 GitHub Enterprise instance configured in order to use this method. You currently have {0}.
settings.almintegration.feature.alm_repo_import.disabled_if_multiple_github_instances=Connecting to multiple GitHub instances will deactivate the {feature} feature. Projects will have to be set up manually.
settings.almintegration.feature.alm_repo_import.github.too_many_instances_x=You must have exactly 1 GitHub instance configured in order to use this method. You currently have {0}.
settings.almintegration.feature.alm_repo_import.github.requires_fields=Your configured instance must be provided with the App's {clientId} and {clientSecret}.


@@ -3100,7 +3100,7 @@ onboarding.project_analysis.guide_to_integrate_pipelines=follow the guide to int
onboarding.create_project.setup_manually=Create a project
onboarding.create_project.select_method.manual=Manually
onboarding.create_project.select_method.bitbucket=From Bitbucket Server
onboarding.create_project.select_method.github=From GitHub Enterprise
onboarding.create_project.select_method.github=From GitHub
onboarding.create_project.alm_not_configured=Currently not active
onboarding.create_project.check_alm_supported=Checking if available
onboarding.create_project.project_key=Project key
@@ -3130,7 +3130,7 @@ onboarding.create_project.too_many_alm_instances.bitbucket=You must have exactly
onboarding.create_project.too_many_alm_instances.github=You must have exactly 1 Bitbucket Server instance configured in order to use this method.
onboarding.create_project.alm_instances_count_X=You currently have {0}.
onboarding.create_project.zero_alm_instances.bitbucket=You must first configure a Bitbucket Server instance.
onboarding.create_project.zero_alm_instances.github=You must first configure a GitHub Enterprise instance.
onboarding.create_project.zero_alm_instances.github=You must first configure a GitHub instance.
onboarding.create_project.no_bbs_binding=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method, but none were found. Either create the project manually, or contact your system administrator.
onboarding.create_project.no_bbs_binding.admin=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method. You can configure instances under {url}.
onboarding.create_project.enter_pat=Enter personal access token
@@ -3151,12 +3151,12 @@ onboarding.create_project.import_selected_repo=Set up selected repository
onboarding.create_project.go_to_project=Go to project
onboarding.create_project.github.title=Which GitHub repository do you want to setup?
onboarding.create_project.github.choose_organization=Choose organization
onboarding.create_project.github.warning.title=Could not connect to GitHub Enterprise
onboarding.create_project.github.warning.message=Please contact an administrator to configure GitHub Enterprise integration.
onboarding.create_project.github.warning.message_admin=Please make sure a GitHub Enterprise instance is configured in the {link} to create a new project from a repository.
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_admin=Please make sure the GitHub instance is correctly configured in the {link} to create a new project from a repository.
onboarding.create_project.github.warning.message_admin.link=ALM integration settings
onboarding.create_project.github.no_orgs=We couldn't load any organizations with your key. Contact an administrator.
onboarding.create_project.github.no_orgs_admin=We couldn't load any organizations with your key. Check the GitHub Enterprise instance configured in the {link}.
onboarding.create_project.github.no_orgs_admin=We couldn't load any organizations. Make sure the GitHub App is installed in at least one organization and check the GitHub instance configuration in the {link}.

onboarding.create_organization.page.header=Create Organization
onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects.

+ 14
- 0
sonar-ws/src/main/protobuf/ws-alm_integrations.proto 查看文件

@@ -65,3 +65,17 @@ message GithubEnterpriseOrganization {
optional string key = 1;
optional string name = 2;
}

// WS api/alm_integrations/list_github_enterprise_repositories
message ListGithubEnterpriseRepositoriesWsResponse {
optional sonarqube.ws.commons.Paging paging = 1;
repeated GithubEnterpriseRepository repositories = 2;
}

message GithubEnterpriseRepository {
optional int64 id = 1;
optional string key = 2;
optional string name = 3;
optional string url = 4;
optional string sqProjectKey = 5;
}

Loading…
取消
儲存