fixup! SONAR-13475 - List Github Enterprise repositories API (#2883)tags/8.4.0.35506
@@ -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)); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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 | |||
( |
@@ -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); |
@@ -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); | |||
} |
@@ -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); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} |
@@ -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> | |||
); | |||
} |
@@ -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()} |
@@ -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']> = {}) { |
@@ -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} | |||
/> | |||
); |
@@ -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()} |
@@ -8,6 +8,7 @@ exports[`should render correctly 1`] = ` | |||
"key": "foo", | |||
} | |||
} | |||
canAdmin={false} | |||
importing={false} | |||
loading={true} | |||
onImportRepository={[Function]} |
@@ -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> |
@@ -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> |
@@ -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. |
@@ -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; | |||
} |