fixup! SONAR-13475 - List Github Enterprise repositories API (#2883)tags/8.4.0.35506
import java.util.List; | import java.util.List; | ||||
import java.util.Optional; | import java.util.Optional; | ||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.stream.Collectors; | |||||
import org.sonar.api.utils.System2; | import org.sonar.api.utils.System2; | ||||
import org.sonar.core.util.UuidFactory; | import org.sonar.core.util.UuidFactory; | ||||
import org.sonar.db.Dao; | import org.sonar.db.Dao; | ||||
import org.sonar.db.DbSession; | import org.sonar.db.DbSession; | ||||
import org.sonar.db.alm.AlmAppInstallDto; | |||||
import org.sonar.db.organization.OrganizationDto; | |||||
import org.sonar.db.project.ProjectDto; | import org.sonar.db.project.ProjectDto; | ||||
import static org.sonar.db.DatabaseUtils.executeLargeInputs; | import static org.sonar.db.DatabaseUtils.executeLargeInputs; | ||||
public List<ProjectAlmSettingDto> selectByAlmSettingAndSlugs(DbSession dbSession, AlmSettingDto almSettingDto, Set<String> almSlugs) { | public List<ProjectAlmSettingDto> selectByAlmSettingAndSlugs(DbSession dbSession, AlmSettingDto almSettingDto, Set<String> almSlugs) { | ||||
return executeLargeInputs(almSlugs, slugs -> getMapper(dbSession).selectByAlmSettingAndSlugs(almSettingDto.getUuid(), slugs)); | 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)); | |||||
} | |||||
} | } |
void deleteByAlmSettingUuid(@Param("almSettingUuid") String almSettingUuid); | void deleteByAlmSettingUuid(@Param("almSettingUuid") String almSettingUuid); | ||||
List<ProjectAlmSettingDto> selectByAlmSettingAndSlugs(@Param("almSettingUuid") String almSettingUuid, @Param("slugs") List<String> slugs); | List<ProjectAlmSettingDto> selectByAlmSettingAndSlugs(@Param("almSettingUuid") String almSettingUuid, @Param("slugs") List<String> slugs); | ||||
List<ProjectAlmSettingDto> selectByAlmSettingAndRepos(@Param("almSettingUuid") String almSettingUuid, @Param("repos") List<String> repos); | |||||
} | } |
</foreach> | </foreach> | ||||
</select> | </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 id="insert" parameterType="Map" useGeneratedKeys="false"> | ||||
INSERT INTO project_alm_settings | INSERT INTO project_alm_settings | ||||
( | ( |
assertThat(underTest.selectByAlmSettingAndSlugs(dbSession, almSettingsDto, new HashSet<>())).isEmpty(); | 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 | @Test | ||||
public void update_existing_binding() { | public void update_existing_binding() { | ||||
when(uuidFactory.create()).thenReturn(A_UUID); | when(uuidFactory.create()).thenReturn(A_UUID); |
}); | }); | ||||
} | } | ||||
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 }); | 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( | export function getGithubOrganizations( | ||||
almSetting: string, | almSetting: string, | ||||
token: string | token: string | ||||
): Promise<{ organizations: GithubOrganization[] }> { | ): Promise<{ organizations: GithubOrganization[] }> { | ||||
return getJSON('/api/alm_integrations/list_github_enterprise_organizations', { | |||||
return getJSON('/api/alm_integrations/list_github_organizations', { | |||||
almSetting, | almSetting, | ||||
token | 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, | almSetting, | ||||
organization, | organization, | ||||
p, | p, | ||||
query: query || undefined | |||||
}); | |||||
ps, | |||||
q: query || undefined | |||||
}).catch(throwGlobalError); | |||||
} | } |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { connect } from 'react-redux'; | |||||
import { WithRouterProps } from 'react-router'; | import { WithRouterProps } from 'react-router'; | ||||
import { | import { | ||||
checkPersonalAccessTokenIsValid, | checkPersonalAccessTokenIsValid, | ||||
searchForBitbucketServerRepositories, | searchForBitbucketServerRepositories, | ||||
setAlmPersonalAccessToken | setAlmPersonalAccessToken | ||||
} from '../../../api/alm-integrations'; | } from '../../../api/alm-integrations'; | ||||
import { getAppState, Store } from '../../../store/rootReducer'; | |||||
import { | import { | ||||
BitbucketProject, | BitbucketProject, | ||||
BitbucketProjectRepositories, | BitbucketProjectRepositories, | ||||
import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer'; | import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer'; | ||||
interface Props extends Pick<WithRouterProps, 'location'> { | interface Props extends Pick<WithRouterProps, 'location'> { | ||||
canAdmin: boolean; | |||||
bitbucketSettings: AlmSettingsInstance[]; | bitbucketSettings: AlmSettingsInstance[]; | ||||
canAdmin?: boolean; | |||||
loadingBindings: boolean; | loadingBindings: boolean; | ||||
onProjectCreate: (projectKeys: string[]) => void; | onProjectCreate: (projectKeys: string[]) => void; | ||||
} | } | ||||
tokenValidationFailed: boolean; | tokenValidationFailed: boolean; | ||||
} | } | ||||
export class BitbucketProjectCreate extends React.PureComponent<Props, State> { | |||||
export default class BitbucketProjectCreate extends React.PureComponent<Props, State> { | |||||
mounted = false; | mounted = false; | ||||
constructor(props: Props) { | constructor(props: Props) { | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
const mapStateToProps = (state: Store) => { | |||||
const { canAdmin } = getAppState(state); | |||||
return { canAdmin }; | |||||
}; | |||||
export default connect(mapStateToProps)(BitbucketProjectCreate); |
export class CreateProjectPage extends React.PureComponent<Props, State> { | export class CreateProjectPage extends React.PureComponent<Props, State> { | ||||
mounted = false; | mounted = false; | ||||
state: State = { bitbucketSettings: [], githubSettings: [], loading: false }; | |||||
state: State = { bitbucketSettings: [], githubSettings: [], loading: true }; | |||||
componentDidMount() { | componentDidMount() { | ||||
const { | const { | ||||
}); | }); | ||||
}; | }; | ||||
handleProjectCreate = (projectKeys: string[]) => { | |||||
if (projectKeys.length === 1) { | |||||
this.props.router.push(getProjectUrl(projectKeys[0])); | |||||
} | |||||
}; | |||||
handleModeSelect = (mode: CreateProjectModes) => { | handleModeSelect = (mode: CreateProjectModes) => { | ||||
const { router, location } = this.props; | const { router, location } = this.props; | ||||
router.push({ | router.push({ | ||||
}); | }); | ||||
}; | }; | ||||
handleProjectCreate = (projectKeys: string[]) => { | |||||
if (projectKeys.length === 1) { | |||||
this.props.router.push(getProjectUrl(projectKeys[0])); | |||||
} | |||||
}; | |||||
renderForm(mode?: CreateProjectModes) { | renderForm(mode?: CreateProjectModes) { | ||||
const { | const { | ||||
appState: { canAdmin }, | appState: { canAdmin }, | ||||
location | |||||
location, | |||||
router | |||||
} = this.props; | } = this.props; | ||||
const { bitbucketSettings, githubSettings, loading } = this.state; | const { bitbucketSettings, githubSettings, loading } = this.state; | ||||
case CreateProjectModes.BitbucketServer: { | case CreateProjectModes.BitbucketServer: { | ||||
return ( | return ( | ||||
<BitbucketProjectCreate | <BitbucketProjectCreate | ||||
canAdmin={!!canAdmin} | |||||
bitbucketSettings={bitbucketSettings} | bitbucketSettings={bitbucketSettings} | ||||
loadingBindings={loading} | loadingBindings={loading} | ||||
location={location} | location={location} | ||||
return ( | return ( | ||||
<GitHubProjectCreate | <GitHubProjectCreate | ||||
canAdmin={!!canAdmin} | canAdmin={!!canAdmin} | ||||
code={location.query?.code} | |||||
settings={githubSettings[0]} | |||||
loadingBindings={loading} | |||||
location={location} | |||||
onProjectCreate={this.handleProjectCreate} | |||||
router={router} | |||||
settings={githubSettings} | |||||
/> | /> | ||||
); | ); | ||||
} | } |
*/ | */ | ||||
import { debounce } from 'lodash'; | import { debounce } from 'lodash'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { WithRouterProps } from 'react-router'; | |||||
import { getHostUrl } from 'sonar-ui-common/helpers/urls'; | import { getHostUrl } from 'sonar-ui-common/helpers/urls'; | ||||
import { | import { | ||||
getGithubClientId, | getGithubClientId, | ||||
getGithubOrganizations, | getGithubOrganizations, | ||||
getGithubRepositories | |||||
getGithubRepositories, | |||||
importGithubRepository | |||||
} from '../../../api/alm-integrations'; | } from '../../../api/alm-integrations'; | ||||
import { GithubOrganization, GithubRepository } from '../../../types/alm-integration'; | import { GithubOrganization, GithubRepository } from '../../../types/alm-integration'; | ||||
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; | import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; | ||||
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer'; | import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer'; | ||||
interface Props { | |||||
interface Props extends Pick<WithRouterProps, 'location' | 'router'> { | |||||
canAdmin: boolean; | canAdmin: boolean; | ||||
code?: string; | |||||
settings?: AlmSettingsInstance; | |||||
loadingBindings: boolean; | |||||
onProjectCreate: (projectKeys: string[]) => void; | |||||
settings: AlmSettingsInstance[]; | |||||
} | } | ||||
interface State { | interface State { | ||||
error: boolean; | error: boolean; | ||||
loading: boolean; | |||||
importing: boolean; | |||||
loadingOrganizations: boolean; | |||||
loadingRepositories: boolean; | loadingRepositories: boolean; | ||||
organizations: GithubOrganization[]; | organizations: GithubOrganization[]; | ||||
repositoryPaging: T.Paging; | repositoryPaging: T.Paging; | ||||
searchQuery: string; | searchQuery: string; | ||||
selectedOrganization?: GithubOrganization; | selectedOrganization?: GithubOrganization; | ||||
selectedRepository?: GithubRepository; | selectedRepository?: GithubRepository; | ||||
settings?: AlmSettingsInstance; | |||||
} | } | ||||
const REPOSITORY_PAGE_SIZE = 30; | const REPOSITORY_PAGE_SIZE = 30; | ||||
this.state = { | this.state = { | ||||
error: false, | error: false, | ||||
loading: true, | |||||
importing: false, | |||||
loadingOrganizations: true, | |||||
loadingRepositories: false, | loadingRepositories: false, | ||||
organizations: [], | organizations: [], | ||||
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] | |||||
}; | }; | ||||
this.triggerSearch = debounce(this.triggerSearch, 250); | this.triggerSearch = debounce(this.triggerSearch, 250); | ||||
} | } | ||||
componentDidUpdate(prevProps: Props) { | 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()); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
async initialize() { | 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 }); | this.setState({ error: true }); | ||||
return; | return; | ||||
} else { | } else { | ||||
this.setState({ error: false }); | this.setState({ error: false }); | ||||
} | } | ||||
const code = location.query?.code; | |||||
try { | try { | ||||
if (!code) { | if (!code) { | ||||
await this.redirectToGithub(settings); | await this.redirectToGithub(settings); | ||||
} else { | } else { | ||||
delete location.query.code; | |||||
router.replace(location); | |||||
await this.fetchOrganizations(settings, code); | await this.fetchOrganizations(settings, code); | ||||
} | } | ||||
} catch (e) { | } catch (e) { | ||||
} | } | ||||
async redirectToGithub(settings: AlmSettingsInstance) { | async redirectToGithub(settings: AlmSettingsInstance) { | ||||
if (!settings.url) { | |||||
return; | |||||
} | |||||
const { clientId } = await getGithubClientId(settings.key); | const { clientId } = await getGithubClientId(settings.key); | ||||
if (!clientId) { | |||||
this.setState({ error: true }); | |||||
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: `${getHostUrl()}/projects/create?mode=${AlmKeys.GitHub}` } | ||||
.map(({ param, value }) => `${param}=${value}`) | .map(({ param, value }) => `${param}=${value}`) | ||||
.join('&'); | .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) { | async fetchOrganizations(settings: AlmSettingsInstance, token: string) { | ||||
const { organizations } = await getGithubOrganizations(settings.key, token); | const { organizations } = await getGithubOrganizations(settings.key, token); | ||||
if (this.mounted) { | if (this.mounted) { | ||||
this.setState({ loading: 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.props; | |||||
const { settings } = this.state; | |||||
if (!settings) { | if (!settings) { | ||||
this.setState({ error: true }); | this.setState({ error: true }); | ||||
this.setState({ loadingRepositories: true }); | 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) => { | triggerSearch = (query: string) => { | ||||
const { selectedOrganization } = this.state; | const { selectedOrganization } = this.state; | ||||
if (selectedOrganization) { | if (selectedOrganization) { | ||||
this.setState({ selectedRepository: undefined }); | |||||
this.fetchRepositories({ organizationKey: selectedOrganization.key, query }); | this.fetchRepositories({ organizationKey: selectedOrganization.key, query }); | ||||
} | } | ||||
}; | }; | ||||
handleSelectOrganization = (key: string) => { | handleSelectOrganization = (key: string) => { | ||||
this.setState(({ organizations }) => ({ | this.setState(({ organizations }) => ({ | ||||
searchQuery: '', | |||||
selectedRepository: undefined, | |||||
selectedOrganization: organizations.find(o => o.key === key) | selectedOrganization: organizations.find(o => o.key === key) | ||||
})); | })); | ||||
this.fetchRepositories({ organizationKey: key }); | this.fetchRepositories({ organizationKey: key }); | ||||
} | } | ||||
}; | }; | ||||
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() { | render() { | ||||
const { canAdmin } = this.props; | |||||
const { canAdmin, loadingBindings } = this.props; | |||||
const { | const { | ||||
error, | error, | ||||
loading, | |||||
importing, | |||||
loadingOrganizations, | |||||
loadingRepositories, | loadingRepositories, | ||||
organizations, | organizations, | ||||
repositoryPaging, | repositoryPaging, | ||||
selectedOrganization, | selectedOrganization, | ||||
selectedRepository | selectedRepository | ||||
} = this.state; | } = this.state; | ||||
return ( | return ( | ||||
<GitHubProjectCreateRenderer | <GitHubProjectCreateRenderer | ||||
canAdmin={canAdmin} | canAdmin={canAdmin} | ||||
error={error} | error={error} | ||||
loading={loading} | |||||
importing={importing} | |||||
loadingBindings={loadingBindings} | |||||
loadingOrganizations={loadingOrganizations} | |||||
loadingRepositories={loadingRepositories} | loadingRepositories={loadingRepositories} | ||||
onImportRepository={this.handleImportRepository} | |||||
onLoadMore={this.handleLoadMore} | onLoadMore={this.handleLoadMore} | ||||
onSearch={this.handleSearch} | onSearch={this.handleSearch} | ||||
onSelectOrganization={this.handleSelectOrganization} | onSelectOrganization={this.handleSelectOrganization} |
import * as React from 'react'; | import * as React from 'react'; | ||||
import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||
import { Link } from 'react-router'; | import { Link } from 'react-router'; | ||||
import { Button } from 'sonar-ui-common/components/controls/buttons'; | |||||
import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | ||||
import Radio from 'sonar-ui-common/components/controls/Radio'; | import Radio from 'sonar-ui-common/components/controls/Radio'; | ||||
import SearchBox from 'sonar-ui-common/components/controls/SearchBox'; | import SearchBox from 'sonar-ui-common/components/controls/SearchBox'; | ||||
export interface GitHubProjectCreateRendererProps { | export interface GitHubProjectCreateRendererProps { | ||||
canAdmin: boolean; | canAdmin: boolean; | ||||
error: boolean; | error: boolean; | ||||
loading: boolean; | |||||
importing: boolean; | |||||
loadingBindings: boolean; | |||||
loadingOrganizations: boolean; | |||||
loadingRepositories: boolean; | loadingRepositories: boolean; | ||||
onImportRepository: () => void; | |||||
onLoadMore: () => void; | onLoadMore: () => void; | ||||
onSearch: (q: string) => void; | onSearch: (q: string) => void; | ||||
onSelectOrganization: (key: string) => void; | onSelectOrganization: (key: string) => void; | ||||
return { value: key, label: name }; | 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 { | const { | ||||
canAdmin, | |||||
error, | |||||
loading, | |||||
importing, | |||||
loadingRepositories, | loadingRepositories, | ||||
organizations, | |||||
repositories, | repositories, | ||||
repositoryPaging, | repositoryPaging, | ||||
searchQuery, | searchQuery, | ||||
selectedRepository | selectedRepository | ||||
} = props; | } = 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 ( | return ( | ||||
<div> | <div> | ||||
<CreateProjectPageHeader | <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={ | title={ | ||||
<span className="text-middle display-flex-center"> | <span className="text-middle display-flex-center"> | ||||
<img | <img | ||||
</div> | </div> | ||||
</div> | </div> | ||||
) : ( | ) : ( | ||||
<DeferredSpinner loading={loading}> | |||||
<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> | ||||
{organizations.length > 0 ? ( | {organizations.length > 0 ? ( | ||||
<SearchSelect | <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} | minimumQueryLength={0} | ||||
onSelect={({ value }) => props.onSelectOrganization(value)} | onSelect={({ value }) => props.onSelectOrganization(value)} | ||||
value={selectedOrganization && orgToOption(selectedOrganization)} | value={selectedOrganization && orgToOption(selectedOrganization)} | ||||
/> | /> | ||||
) : ( | ) : ( | ||||
!loading && ( | |||||
!loadingOrganizations && ( | |||||
<Alert className="spacer-top" variant="error"> | <Alert className="spacer-top" variant="error"> | ||||
{canAdmin ? ( | {canAdmin ? ( | ||||
<FormattedMessage | <FormattedMessage | ||||
</DeferredSpinner> | </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> | </div> | ||||
); | ); | ||||
} | } |
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; | import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; | ||||
import { mockLocation } from '../../../../helpers/testMocks'; | import { mockLocation } from '../../../../helpers/testMocks'; | ||||
import { AlmKeys } from '../../../../types/alm-settings'; | import { AlmKeys } from '../../../../types/alm-settings'; | ||||
import { BitbucketProjectCreate } from '../BitbucketProjectCreate'; | |||||
import BitbucketProjectCreate from '../BitbucketProjectCreate'; | |||||
jest.mock('../../../../api/alm-integrations', () => { | jest.mock('../../../../api/alm-integrations', () => { | ||||
const { mockBitbucketProject, mockBitbucketRepository } = jest.requireActual( | const { mockBitbucketProject, mockBitbucketRepository } = jest.requireActual( | ||||
function shallowRender(props: Partial<BitbucketProjectCreate['props']> = {}) { | function shallowRender(props: Partial<BitbucketProjectCreate['props']> = {}) { | ||||
return shallow<BitbucketProjectCreate>( | return shallow<BitbucketProjectCreate>( | ||||
<BitbucketProjectCreate | <BitbucketProjectCreate | ||||
canAdmin={false} | |||||
bitbucketSettings={[mockAlmSettingsInstance({ alm: AlmKeys.Bitbucket, key: 'foo' })]} | bitbucketSettings={[mockAlmSettingsInstance({ alm: AlmKeys.Bitbucket, key: 'foo' })]} | ||||
loadingBindings={false} | loadingBindings={false} | ||||
location={mockLocation()} | location={mockLocation()} |
}); | }); | ||||
it('should render correctly if the manual method is selected', () => { | 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', () => { | 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', () => { | 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']> = {}) { | function shallowRender(props: Partial<CreateProjectPage['props']> = {}) { |
import { | import { | ||||
getGithubClientId, | getGithubClientId, | ||||
getGithubOrganizations, | getGithubOrganizations, | ||||
getGithubRepositories | |||||
getGithubRepositories, | |||||
importGithubRepository | |||||
} from '../../../../api/alm-integrations'; | } from '../../../../api/alm-integrations'; | ||||
import { mockGitHubRepository } from '../../../../helpers/mocks/alm-integrations'; | import { mockGitHubRepository } from '../../../../helpers/mocks/alm-integrations'; | ||||
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; | import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; | ||||
import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; | |||||
import GitHubProjectCreate from '../GitHubProjectCreate'; | import GitHubProjectCreate from '../GitHubProjectCreate'; | ||||
jest.mock('../../../../api/alm-integrations', () => ({ | jest.mock('../../../../api/alm-integrations', () => ({ | ||||
getGithubClientId: jest.fn().mockResolvedValue({ clientId: 'client-id-124' }), | getGithubClientId: jest.fn().mockResolvedValue({ clientId: 'client-id-124' }), | ||||
getGithubOrganizations: jest.fn().mockResolvedValue({ organizations: [] }), | 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; | const originalLocation = window.location; | ||||
}); | }); | ||||
it('should handle no settings', async () => { | it('should handle no settings', async () => { | ||||
const wrapper = shallowRender({ settings: undefined }); | |||||
const wrapper = shallowRender({ settings: [] }); | |||||
await waitAndUpdate(wrapper); | await waitAndUpdate(wrapper); | ||||
expect(wrapper.state().error).toBe(true); | expect(wrapper.state().error).toBe(true); | ||||
}); | }); | ||||
expect(window.location.replace).toBeCalled(); | 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 () => { | it('should fetch organizations when code', async () => { | ||||
const organizations = [ | const organizations = [ | ||||
{ key: '1', name: 'org1' }, | { key: '1', name: 'org1' }, | ||||
{ key: '2', name: 'org2' } | { key: '2', name: 'org2' } | ||||
]; | ]; | ||||
(getGithubOrganizations as jest.Mock).mockResolvedValueOnce({ organizations }); | (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); | await waitAndUpdate(wrapper); | ||||
expect(replace).toBeCalled(); | |||||
expect(getGithubOrganizations).toBeCalled(); | expect(getGithubOrganizations).toBeCalled(); | ||||
expect(wrapper.state().organizations).toBe(organizations); | expect(wrapper.state().organizations).toBe(organizations); | ||||
}); | }); | ||||
repositories, | repositories, | ||||
paging: { total: 1, pageIndex: 1 } | paging: { total: 1, pageIndex: 1 } | ||||
}); | }); | ||||
const wrapper = shallowRender({ code: '123456' }); | |||||
const wrapper = shallowRender({ location: mockLocation({ query: { code: '123456' } }) }); | |||||
await waitAndUpdate(wrapper); | await waitAndUpdate(wrapper); | ||||
wrapper.instance().handleSelectOrganization('1'); | wrapper.instance().handleSelectOrganization('1'); | ||||
await waitAndUpdate(wrapper); | 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); | expect(wrapper.state().repositories).toEqual(repositories); | ||||
}); | }); | ||||
expect(wrapper.state().selectedRepository).toBe(repo); | 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']> = {}) { | function shallowRender(props: Partial<GitHubProjectCreate['props']> = {}) { | ||||
return shallow<GitHubProjectCreate>( | return shallow<GitHubProjectCreate>( | ||||
<GitHubProjectCreate | <GitHubProjectCreate | ||||
canAdmin={false} | 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} | {...props} | ||||
/> | /> | ||||
); | ); |
it('should render correctly', () => { | it('should render correctly', () => { | ||||
expect(shallowRender()).toMatchSnapshot('default'); | expect(shallowRender()).toMatchSnapshot('default'); | ||||
expect(shallowRender({ loadingBindings: true })).toMatchSnapshot('loading'); | |||||
expect(shallowRender({ error: true })).toMatchSnapshot('error'); | expect(shallowRender({ error: true })).toMatchSnapshot('error'); | ||||
expect(shallowRender({ canAdmin: true, error: true })).toMatchSnapshot('error for admin'); | expect(shallowRender({ canAdmin: true, error: true })).toMatchSnapshot('error for admin'); | ||||
}); | }); | ||||
describe('callback', () => { | describe('callback', () => { | ||||
const onImportRepository = jest.fn(); | |||||
const onSelectOrganization = jest.fn(); | const onSelectOrganization = jest.fn(); | ||||
const onSelectRepository = jest.fn(); | const onSelectRepository = jest.fn(); | ||||
const onSearch = jest.fn(); | const onSearch = jest.fn(); | ||||
const org = { key: 'o1', name: 'org' }; | const org = { key: 'o1', name: 'org' }; | ||||
const wrapper = shallowRender({ | const wrapper = shallowRender({ | ||||
onImportRepository, | |||||
onSelectOrganization, | onSelectOrganization, | ||||
onSelectRepository, | onSelectRepository, | ||||
onSearch, | onSearch, | ||||
it('should be called when org is selected', () => { | it('should be called when org is selected', () => { | ||||
const value = 'o1'; | const value = 'o1'; | ||||
wrapper.find(SearchSelect).props().onSelect!({ value }); | |||||
wrapper.find(SearchSelect).simulate('select', { value }); | |||||
expect(onSelectOrganization).toBeCalledWith(value); | expect(onSelectOrganization).toBeCalledWith(value); | ||||
}); | }); | ||||
<GitHubProjectCreateRenderer | <GitHubProjectCreateRenderer | ||||
canAdmin={false} | canAdmin={false} | ||||
error={false} | error={false} | ||||
loading={false} | |||||
importing={false} | |||||
loadingBindings={false} | |||||
loadingOrganizations={false} | |||||
loadingRepositories={false} | loadingRepositories={false} | ||||
onImportRepository={jest.fn()} | |||||
onLoadMore={jest.fn()} | onLoadMore={jest.fn()} | ||||
onSearch={jest.fn()} | onSearch={jest.fn()} | ||||
onSelectOrganization={jest.fn()} | onSelectOrganization={jest.fn()} |
"key": "foo", | "key": "foo", | ||||
} | } | ||||
} | } | ||||
canAdmin={false} | |||||
importing={false} | importing={false} | ||||
loading={true} | loading={true} | ||||
onImportRepository={[Function]} | onImportRepository={[Function]} |
className="page page-limited huge-spacer-bottom position-relative" | className="page page-limited huge-spacer-bottom position-relative" | ||||
id="create-project" | id="create-project" | ||||
> | > | ||||
<Connect(BitbucketProjectCreate) | |||||
<BitbucketProjectCreate | |||||
bitbucketSettings={Array []} | bitbucketSettings={Array []} | ||||
canAdmin={false} | |||||
loadingBindings={true} | loadingBindings={true} | ||||
location={ | location={ | ||||
Object { | Object { | ||||
> | > | ||||
<GitHubProjectCreate | <GitHubProjectCreate | ||||
canAdmin={false} | 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> | </div> | ||||
</Fragment> | </Fragment> |
</div> | </div> | ||||
`; | `; | ||||
exports[`should render correctly: loading 1`] = ` | |||||
<DeferredSpinner | |||||
timeout={100} | |||||
/> | |||||
`; | |||||
exports[`should render correctly: no repositories 1`] = ` | exports[`should render correctly: no repositories 1`] = ` | ||||
<div> | <div> | ||||
<CreateProjectPageHeader | <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={ | title={ | ||||
<span | <span | ||||
className="text-middle display-flex-center" | className="text-middle display-flex-center" | ||||
exports[`should render correctly: repositories 1`] = ` | exports[`should render correctly: repositories 1`] = ` | ||||
<div> | <div> | ||||
<CreateProjectPageHeader | <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={ | title={ | ||||
<span | <span | ||||
className="text-middle display-flex-center" | className="text-middle display-flex-center" | ||||
</div> | </div> | ||||
<Radio | <Radio | ||||
checked={false} | checked={false} | ||||
className="spacer-top spacer-bottom padded github-repository" | |||||
className="spacer-top spacer-bottom padded create-project-github-repository" | |||||
disabled={false} | disabled={false} | ||||
key="repo1" | key="repo1" | ||||
onCheck={[MockFunction]} | onCheck={[MockFunction]} | ||||
title="repository 1" | title="repository 1" | ||||
> | > | ||||
<div | <div | ||||
className="overflow-hidden text-ellipsis" | |||||
className="text-ellipsis" | |||||
> | > | ||||
repository 1 | repository 1 | ||||
</div> | </div> | ||||
</Radio> | </Radio> | ||||
<Radio | <Radio | ||||
checked={true} | checked={true} | ||||
className="spacer-top spacer-bottom padded github-repository" | |||||
className="spacer-top spacer-bottom padded create-project-github-repository" | |||||
disabled={true} | disabled={true} | ||||
key="repo2" | key="repo2" | ||||
onCheck={[MockFunction]} | onCheck={[MockFunction]} | ||||
title="repository 1" | title="repository 1" | ||||
> | > | ||||
<div | <div | ||||
className="overflow-hidden text-ellipsis" | |||||
className="text-ellipsis" | |||||
> | > | ||||
repository 1 | repository 1 | ||||
</div> | </div> | ||||
<em | <em | ||||
className="notice text-muted-2 small display-flex-center" | className="notice text-muted-2 small display-flex-center" | ||||
> | > | ||||
onboarding.create_project.already_imported | |||||
onboarding.create_project.repository_imported | |||||
<CheckIcon | <CheckIcon | ||||
className="little-spacer-left" | className="little-spacer-left" | ||||
size={12} | size={12} | ||||
</Radio> | </Radio> | ||||
<Radio | <Radio | ||||
checked={true} | checked={true} | ||||
className="spacer-top spacer-bottom padded github-repository" | |||||
className="spacer-top spacer-bottom padded create-project-github-repository" | |||||
disabled={false} | disabled={false} | ||||
key="repo3" | key="repo3" | ||||
onCheck={[MockFunction]} | onCheck={[MockFunction]} | ||||
title="repository 1" | title="repository 1" | ||||
> | > | ||||
<div | <div | ||||
className="overflow-hidden text-ellipsis" | |||||
className="text-ellipsis" | |||||
> | > | ||||
repository 1 | repository 1 | ||||
</div> | </div> |
settings.almintegration.feature.alm_repo_import.title=Import repositories from your ALM | 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.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_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}. | settings.almintegration.feature.alm_repo_import.github.requires_fields=Your configured instance must be provided with the App's {clientId} and {clientSecret}. | ||||
onboarding.create_project.setup_manually=Create a project | onboarding.create_project.setup_manually=Create a project | ||||
onboarding.create_project.select_method.manual=Manually | onboarding.create_project.select_method.manual=Manually | ||||
onboarding.create_project.select_method.bitbucket=From Bitbucket Server | 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.alm_not_configured=Currently not active | ||||
onboarding.create_project.check_alm_supported=Checking if available | onboarding.create_project.check_alm_supported=Checking if available | ||||
onboarding.create_project.project_key=Project key | onboarding.create_project.project_key=Project key | ||||
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.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.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.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=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.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 | onboarding.create_project.enter_pat=Enter personal access token | ||||
onboarding.create_project.go_to_project=Go to project | 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.title=Which GitHub repository do you want to setup? | ||||
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 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.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=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.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. | onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects. |
optional string key = 1; | optional string key = 1; | ||||
optional string name = 2; | 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; | |||||
} |