aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorDuarte Meneses <duarte.meneses@sonarsource.com>2020-06-29 08:22:22 -0500
committersonartech <sonartech@sonarsource.com>2020-07-01 20:05:53 +0000
commitdb5fc175198e954404be2a9d73d63b7e3c19af49 (patch)
tree537270857368638dc50152bddc9345f64516827c /server/sonar-web/src/main/js
parent46d5b7c51b9c41d35d90416281144385555b9059 (diff)
downloadsonarqube-db5fc175198e954404be2a9d73d63b7e3c19af49.tar.gz
sonarqube-db5fc175198e954404be2a9d73d63b7e3c19af49.zip
SONAR-13475 - List Github Enterprise repositories API (#2883)
fixup! SONAR-13475 - List Github Enterprise repositories API (#2883)
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/api/alm-integrations.ts41
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx127
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx170
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx81
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap32
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap56
12 files changed, 434 insertions, 163 deletions
diff --git a/server/sonar-web/src/main/js/api/alm-integrations.ts b/server/sonar-web/src/main/js/api/alm-integrations.ts
index 3b5712b5d22..b0007b3c480 100644
--- a/server/sonar-web/src/main/js/api/alm-integrations.ts
+++ b/server/sonar-web/src/main/js/api/alm-integrations.ts
@@ -87,30 +87,49 @@ export function searchForBitbucketServerRepositories(
});
}
-export function getGithubClientId(almSetting: string): Promise<{ clientId: string }> {
+export function getGithubClientId(almSetting: string): Promise<{ clientId?: string }> {
return getJSON('/api/alm_integrations/get_github_client_id', { almSetting });
}
+export function importGithubRepository(
+ almSetting: string,
+ organization: string,
+ repositoryKey: string
+): Promise<{ project: ProjectBase }> {
+ return postJSON('/api/alm_integrations/import_github_project', {
+ almSetting,
+ organization,
+ repositoryKey
+ }).catch(throwGlobalError);
+}
+
export function getGithubOrganizations(
almSetting: string,
token: string
): Promise<{ organizations: GithubOrganization[] }> {
- return getJSON('/api/alm_integrations/list_github_enterprise_organizations', {
+ return getJSON('/api/alm_integrations/list_github_organizations', {
almSetting,
token
+ }).catch((response?: Response) => {
+ if (response && response.status !== 400) {
+ throwGlobalError(response);
+ }
});
}
-export function getGithubRepositories(
- almSetting: string,
- organization: string,
- p = 1,
- query?: string
-): Promise<{ repositories: GithubRepository[]; paging: T.Paging }> {
- return getJSON('/api/alm_integrations/list_github_enterprise_repositories', {
+export function getGithubRepositories(data: {
+ almSetting: string;
+ organization: string;
+ ps: number;
+ p?: number;
+ query?: string;
+}): Promise<{ repositories: GithubRepository[]; paging: T.Paging }> {
+ const { almSetting, organization, ps, p = 1, query } = data;
+ return getJSON('/api/alm_integrations/list_github_repositories', {
almSetting,
organization,
p,
- query: query || undefined
- });
+ ps,
+ q: query || undefined
+ }).catch(throwGlobalError);
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx
index ef6d825005a..64fbcaa5930 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { connect } from 'react-redux';
import { WithRouterProps } from 'react-router';
import {
checkPersonalAccessTokenIsValid,
@@ -28,7 +27,6 @@ import {
searchForBitbucketServerRepositories,
setAlmPersonalAccessToken
} from '../../../api/alm-integrations';
-import { getAppState, Store } from '../../../store/rootReducer';
import {
BitbucketProject,
BitbucketProjectRepositories,
@@ -38,8 +36,8 @@ import { AlmSettingsInstance } from '../../../types/alm-settings';
import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer';
interface Props extends Pick<WithRouterProps, 'location'> {
+ canAdmin: boolean;
bitbucketSettings: AlmSettingsInstance[];
- canAdmin?: boolean;
loadingBindings: boolean;
onProjectCreate: (projectKeys: string[]) => void;
}
@@ -58,7 +56,7 @@ interface State {
tokenValidationFailed: boolean;
}
-export class BitbucketProjectCreate extends React.PureComponent<Props, State> {
+export default class BitbucketProjectCreate extends React.PureComponent<Props, State> {
mounted = false;
constructor(props: Props) {
@@ -285,10 +283,3 @@ export class BitbucketProjectCreate extends React.PureComponent<Props, State> {
);
}
}
-
-const mapStateToProps = (state: Store) => {
- const { canAdmin } = getAppState(state);
- return { canAdmin };
-};
-
-export default connect(mapStateToProps)(BitbucketProjectCreate);
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
index 5737a060933..3b4183beae4 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
@@ -47,7 +47,7 @@ interface State {
export class CreateProjectPage extends React.PureComponent<Props, State> {
mounted = false;
- state: State = { bitbucketSettings: [], githubSettings: [], loading: false };
+ state: State = { bitbucketSettings: [], githubSettings: [], loading: true };
componentDidMount() {
const {
@@ -82,12 +82,6 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
});
};
- handleProjectCreate = (projectKeys: string[]) => {
- if (projectKeys.length === 1) {
- this.props.router.push(getProjectUrl(projectKeys[0]));
- }
- };
-
handleModeSelect = (mode: CreateProjectModes) => {
const { router, location } = this.props;
router.push({
@@ -96,10 +90,17 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
});
};
+ handleProjectCreate = (projectKeys: string[]) => {
+ if (projectKeys.length === 1) {
+ this.props.router.push(getProjectUrl(projectKeys[0]));
+ }
+ };
+
renderForm(mode?: CreateProjectModes) {
const {
appState: { canAdmin },
- location
+ location,
+ router
} = this.props;
const { bitbucketSettings, githubSettings, loading } = this.state;
@@ -107,6 +108,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
case CreateProjectModes.BitbucketServer: {
return (
<BitbucketProjectCreate
+ canAdmin={!!canAdmin}
bitbucketSettings={bitbucketSettings}
loadingBindings={loading}
location={location}
@@ -118,8 +120,11 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
return (
<GitHubProjectCreate
canAdmin={!!canAdmin}
- code={location.query?.code}
- settings={githubSettings[0]}
+ loadingBindings={loading}
+ location={location}
+ onProjectCreate={this.handleProjectCreate}
+ router={router}
+ settings={githubSettings}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx
index 3417541c022..1c03f8c3b50 100644
--- a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx
@@ -19,25 +19,29 @@
*/
import { debounce } from 'lodash';
import * as React from 'react';
+import { WithRouterProps } from 'react-router';
import { getHostUrl } from 'sonar-ui-common/helpers/urls';
import {
getGithubClientId,
getGithubOrganizations,
- getGithubRepositories
+ getGithubRepositories,
+ importGithubRepository
} from '../../../api/alm-integrations';
import { GithubOrganization, GithubRepository } from '../../../types/alm-integration';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
-interface Props {
+interface Props extends Pick<WithRouterProps, 'location' | 'router'> {
canAdmin: boolean;
- code?: string;
- settings?: AlmSettingsInstance;
+ loadingBindings: boolean;
+ onProjectCreate: (projectKeys: string[]) => void;
+ settings: AlmSettingsInstance[];
}
interface State {
error: boolean;
- loading: boolean;
+ importing: boolean;
+ loadingOrganizations: boolean;
loadingRepositories: boolean;
organizations: GithubOrganization[];
repositoryPaging: T.Paging;
@@ -45,6 +49,7 @@ interface State {
searchQuery: string;
selectedOrganization?: GithubOrganization;
selectedRepository?: GithubRepository;
+ settings?: AlmSettingsInstance;
}
const REPOSITORY_PAGE_SIZE = 30;
@@ -57,12 +62,14 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
this.state = {
error: false,
- loading: true,
+ importing: false,
+ loadingOrganizations: true,
loadingRepositories: false,
organizations: [],
repositories: [],
repositoryPaging: { pageSize: REPOSITORY_PAGE_SIZE, total: 0, pageIndex: 1 },
- searchQuery: ''
+ searchQuery: '',
+ settings: props.settings[0]
};
this.triggerSearch = debounce(this.triggerSearch, 250);
@@ -75,8 +82,8 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
}
componentDidUpdate(prevProps: Props) {
- if (!prevProps.settings && this.props.settings) {
- this.initialize();
+ if (prevProps.settings.length === 0 && this.props.settings.length > 0) {
+ this.setState({ settings: this.props.settings[0] }, () => this.initialize());
}
}
@@ -85,19 +92,24 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
}
async initialize() {
- const { code, settings } = this.props;
+ const { location, router } = this.props;
+ const { settings } = this.state;
- if (!settings) {
+ if (!settings || !settings.url) {
this.setState({ error: true });
return;
} else {
this.setState({ error: false });
}
+ const code = location.query?.code;
+
try {
if (!code) {
await this.redirectToGithub(settings);
} else {
+ delete location.query.code;
+ router.replace(location);
await this.fetchOrganizations(settings, code);
}
} catch (e) {
@@ -108,8 +120,17 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
}
async redirectToGithub(settings: AlmSettingsInstance) {
+ if (!settings.url) {
+ return;
+ }
+
const { clientId } = await getGithubClientId(settings.key);
+ if (!clientId) {
+ this.setState({ error: true });
+ return;
+ }
+
const queryParams = [
{ param: 'client_id', value: clientId },
{ param: 'redirect_uri', value: `${getHostUrl()}/projects/create?mode=${AlmKeys.GitHub}` }
@@ -117,20 +138,32 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
.map(({ param, value }) => `${param}=${value}`)
.join('&');
- window.location.replace(`https://github.com/login/oauth/authorize?${queryParams}`);
+ let instanceRootUrl;
+ // Strip the api section from the url, since we're not hitting the api here.
+ if (settings.url.includes('/api/v3')) {
+ // GitHub Enterprise
+ instanceRootUrl = settings.url.replace('/api/v3', '');
+ } else {
+ // github.com
+ instanceRootUrl = settings.url.replace('api.', '');
+ }
+
+ // strip the trailing /
+ instanceRootUrl = instanceRootUrl.replace(/\/$/, '');
+ window.location.replace(`${instanceRootUrl}/login/oauth/authorize?${queryParams}`);
}
async fetchOrganizations(settings: AlmSettingsInstance, token: string) {
const { organizations } = await getGithubOrganizations(settings.key, token);
if (this.mounted) {
- this.setState({ loading: false, organizations });
+ this.setState({ loadingOrganizations: false, organizations });
}
}
async fetchRepositories(params: { organizationKey: string; page?: number; query?: string }) {
const { organizationKey, page = 1, query } = params;
- const { settings } = this.props;
+ const { settings } = this.state;
if (!settings) {
this.setState({ error: true });
@@ -139,26 +172,45 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
this.setState({ loadingRepositories: true });
- const data = await getGithubRepositories(settings.key, organizationKey, page, query);
+ try {
+ const data = await getGithubRepositories({
+ almSetting: settings.key,
+ organization: organizationKey,
+ ps: REPOSITORY_PAGE_SIZE,
+ p: page,
+ query
+ });
- if (this.mounted) {
- this.setState(({ repositories }) => ({
- loadingRepositories: false,
- repositoryPaging: data.paging,
- repositories: page === 1 ? data.repositories : [...repositories, ...data.repositories]
- }));
+ if (this.mounted) {
+ this.setState(({ repositories }) => ({
+ loadingRepositories: false,
+ repositoryPaging: data.paging,
+ repositories: page === 1 ? data.repositories : [...repositories, ...data.repositories]
+ }));
+ }
+ } catch (_) {
+ if (this.mounted) {
+ this.setState({
+ loadingRepositories: false,
+ repositoryPaging: { pageIndex: 1, pageSize: REPOSITORY_PAGE_SIZE, total: 0 },
+ repositories: []
+ });
+ }
}
}
triggerSearch = (query: string) => {
const { selectedOrganization } = this.state;
if (selectedOrganization) {
+ this.setState({ selectedRepository: undefined });
this.fetchRepositories({ organizationKey: selectedOrganization.key, query });
}
};
handleSelectOrganization = (key: string) => {
this.setState(({ organizations }) => ({
+ searchQuery: '',
+ selectedRepository: undefined,
selectedOrganization: organizations.find(o => o.key === key)
}));
this.fetchRepositories({ organizationKey: key });
@@ -187,11 +239,34 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
}
};
+ handleImportRepository = async () => {
+ const { selectedOrganization, selectedRepository, settings } = this.state;
+
+ if (settings && selectedOrganization && selectedRepository) {
+ this.setState({ importing: true });
+
+ try {
+ const { project } = await importGithubRepository(
+ settings.key,
+ selectedOrganization.key,
+ selectedRepository.key
+ );
+
+ this.props.onProjectCreate([project.key]);
+ } finally {
+ if (this.mounted) {
+ this.setState({ importing: false });
+ }
+ }
+ }
+ };
+
render() {
- const { canAdmin } = this.props;
+ const { canAdmin, loadingBindings } = this.props;
const {
error,
- loading,
+ importing,
+ loadingOrganizations,
loadingRepositories,
organizations,
repositoryPaging,
@@ -200,12 +275,16 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
selectedOrganization,
selectedRepository
} = this.state;
+
return (
<GitHubProjectCreateRenderer
canAdmin={canAdmin}
error={error}
- loading={loading}
+ importing={importing}
+ loadingBindings={loadingBindings}
+ loadingOrganizations={loadingOrganizations}
loadingRepositories={loadingRepositories}
+ onImportRepository={this.handleImportRepository}
onLoadMore={this.handleLoadMore}
onSearch={this.handleSearch}
onSelectOrganization={this.handleSelectOrganization}
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx
index 289bdf442c1..77ed4fa16b7 100644
--- a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
import Radio from 'sonar-ui-common/components/controls/Radio';
import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
@@ -35,8 +36,11 @@ import CreateProjectPageHeader from './CreateProjectPageHeader';
export interface GitHubProjectCreateRendererProps {
canAdmin: boolean;
error: boolean;
- loading: boolean;
+ importing: boolean;
+ loadingBindings: boolean;
+ loadingOrganizations: boolean;
loadingRepositories: boolean;
+ onImportRepository: () => void;
onLoadMore: () => void;
onSearch: (q: string) => void;
onSelectOrganization: (key: string) => void;
@@ -53,13 +57,13 @@ function orgToOption({ key, name }: GithubOrganization) {
return { value: key, label: name };
}
-export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRendererProps) {
+const handleSearch = (organizations: GithubOrganization[]) => (q: string) =>
+ Promise.resolve(organizations.filter(o => !q || o.name.includes(q)).map(orgToOption));
+
+function renderRepositoryList(props: GitHubProjectCreateRendererProps) {
const {
- canAdmin,
- error,
- loading,
+ importing,
loadingRepositories,
- organizations,
repositories,
repositoryPaging,
searchQuery,
@@ -67,9 +71,99 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
selectedRepository
} = props;
+ const isChecked = (repository: GithubRepository) =>
+ !!repository.sqProjectKey ||
+ (!!selectedRepository && selectedRepository.key === repository.key);
+
+ const isDisabled = (repository: GithubRepository) =>
+ !!repository.sqProjectKey || loadingRepositories || importing;
+
+ return (
+ selectedOrganization &&
+ repositories && (
+ <div className="boxed-group padded display-flex-wrap">
+ <div className="width-100">
+ <SearchBox
+ className="big-spacer-bottom"
+ onChange={props.onSearch}
+ placeholder={translate('onboarding.create_project.search_repositories')}
+ value={searchQuery}
+ />
+ </div>
+
+ {repositories.length === 0 ? (
+ <div className="padded">
+ <DeferredSpinner loading={loadingRepositories}>
+ {translate('no_results')}
+ </DeferredSpinner>
+ </div>
+ ) : (
+ repositories.map(r => (
+ <Radio
+ className="spacer-top spacer-bottom padded create-project-github-repository"
+ key={r.key}
+ checked={isChecked(r)}
+ disabled={isDisabled(r)}
+ value={r.key}
+ onCheck={props.onSelectRepository}>
+ <div className="big overflow-hidden" title={r.name}>
+ <div className="text-ellipsis">{r.name}</div>
+ {r.sqProjectKey && (
+ <em className="notice text-muted-2 small display-flex-center">
+ {translate('onboarding.create_project.repository_imported')}
+ <CheckIcon className="little-spacer-left" size={12} />
+ </em>
+ )}
+ </div>
+ </Radio>
+ ))
+ )}
+
+ <div className="display-flex-justify-center width-100">
+ <ListFooter
+ count={repositories.length}
+ total={repositoryPaging.total}
+ loadMore={props.onLoadMore}
+ loading={loadingRepositories}
+ />
+ </div>
+ </div>
+ )
+ );
+}
+
+export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRendererProps) {
+ const {
+ canAdmin,
+ error,
+ importing,
+ loadingBindings,
+ loadingOrganizations,
+ organizations,
+ selectedOrganization,
+ selectedRepository
+ } = props;
+
+ if (loadingBindings) {
+ return <DeferredSpinner />;
+ }
+
return (
<div>
<CreateProjectPageHeader
+ additionalActions={
+ selectedOrganization && (
+ <div className="display-flex-center pull-right">
+ <DeferredSpinner className="spacer-right" loading={importing} />
+ <Button
+ className="button-large button-primary"
+ disabled={!selectedRepository || importing}
+ onClick={props.onImportRepository}>
+ {translate('onboarding.create_project.import_selected_repo')}
+ </Button>
+ </div>
+ )
+ }
title={
<span className="text-middle display-flex-center">
<img
@@ -111,23 +205,19 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
</div>
</div>
) : (
- <DeferredSpinner loading={loading}>
+ <DeferredSpinner loading={loadingOrganizations}>
<div className="form-field">
<label>{translate('onboarding.create_project.github.choose_organization')}</label>
{organizations.length > 0 ? (
<SearchSelect
- defaultOptions={organizations.slice(0, 10).map(orgToOption)}
- onSearch={(q: string) =>
- Promise.resolve(
- organizations.filter(o => !q || o.name.includes(q)).map(orgToOption)
- )
- }
+ defaultOptions={organizations.map(orgToOption)}
+ onSearch={handleSearch(organizations)}
minimumQueryLength={0}
onSelect={({ value }) => props.onSelectOrganization(value)}
value={selectedOrganization && orgToOption(selectedOrganization)}
/>
) : (
- !loading && (
+ !loadingOrganizations && (
<Alert className="spacer-top" variant="error">
{canAdmin ? (
<FormattedMessage
@@ -153,57 +243,7 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
</DeferredSpinner>
)}
- {selectedOrganization && repositories && (
- <div className="boxed-group padded display-flex-wrap">
- <div className="width-100">
- <SearchBox
- className="big-spacer-bottom"
- onChange={props.onSearch}
- placeholder={translate('onboarding.create_project.search_repositories')}
- value={searchQuery}
- />
- </div>
-
- {repositories.length === 0 ? (
- <div className="padded">
- <DeferredSpinner loading={loadingRepositories}>
- {translate('no_results')}
- </DeferredSpinner>
- </div>
- ) : (
- repositories.map(r => (
- <Radio
- className="spacer-top spacer-bottom padded create-project-github-repository"
- key={r.key}
- checked={
- !!r.sqProjectKey || (!!selectedRepository && selectedRepository.key === r.key)
- }
- disabled={!!r.sqProjectKey || loadingRepositories || importing}
- value={r.key}
- onCheck={props.onSelectRepository}>
- <div className="big overflow-hidden" title={r.name}>
- <div className="overflow-hidden text-ellipsis">{r.name}</div>
- {r.sqProjectKey && (
- <em className="notice text-muted-2 small display-flex-center">
- {translate('onboarding.create_project.repository_imported')}
- <CheckIcon className="little-spacer-left" size={12} />
- </em>
- )}
- </div>
- </Radio>
- ))
- )}
-
- <div className="display-flex-justify-center width-100">
- <ListFooter
- count={repositories.length}
- total={repositoryPaging.total}
- loadMore={props.onLoadMore}
- loading={loadingRepositories}
- />
- </div>
- </div>
- )}
+ {renderRepositoryList(props)}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx
index 6d1405bb939..b142ef13b16 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx
@@ -33,7 +33,7 @@ import { mockBitbucketRepository } from '../../../../helpers/mocks/alm-integrati
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockLocation } from '../../../../helpers/testMocks';
import { AlmKeys } from '../../../../types/alm-settings';
-import { BitbucketProjectCreate } from '../BitbucketProjectCreate';
+import BitbucketProjectCreate from '../BitbucketProjectCreate';
jest.mock('../../../../api/alm-integrations', () => {
const { mockBitbucketProject, mockBitbucketRepository } = jest.requireActual(
@@ -163,6 +163,7 @@ it('should correctly handle search', async () => {
function shallowRender(props: Partial<BitbucketProjectCreate['props']> = {}) {
return shallow<BitbucketProjectCreate>(
<BitbucketProjectCreate
+ canAdmin={false}
bitbucketSettings={[mockAlmSettingsInstance({ alm: AlmKeys.Bitbucket, key: 'foo' })]}
loadingBindings={false}
location={mockLocation()}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
index 07e45b72fe6..8368b8eb386 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
@@ -43,36 +43,26 @@ it('should render correctly if no branch support', () => {
});
it('should render correctly if the manual method is selected', () => {
- const push = jest.fn();
- const location = { query: { mode: CreateProjectModes.Manual } };
- const wrapper = shallowRender({ router: mockRouter({ push }) });
-
- wrapper.instance().handleModeSelect(CreateProjectModes.Manual);
- expect(push).toBeCalledWith(expect.objectContaining(location));
-
- expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot();
+ expect(
+ shallowRender({
+ location: mockLocation({ query: { mode: CreateProjectModes.Manual } })
+ })
+ ).toMatchSnapshot();
});
it('should render correctly if the BBS method is selected', () => {
- const push = jest.fn();
- const location = { query: { mode: CreateProjectModes.BitbucketServer } };
- const wrapper = shallowRender({ router: mockRouter({ push }) });
-
- wrapper.instance().handleModeSelect(CreateProjectModes.BitbucketServer);
- expect(push).toBeCalledWith(expect.objectContaining(location));
-
- expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot();
+ expect(
+ shallowRender({
+ location: mockLocation({ query: { mode: CreateProjectModes.BitbucketServer } })
+ })
+ ).toMatchSnapshot();
});
it('should render correctly if the GitHub method is selected', () => {
- const push = jest.fn();
- const location = { query: { mode: CreateProjectModes.GitHub } };
- const wrapper = shallowRender({ router: mockRouter({ push }) });
-
- wrapper.instance().handleModeSelect(CreateProjectModes.GitHub);
- expect(push).toBeCalledWith(expect.objectContaining(location));
-
- expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot();
+ const wrapper = shallowRender({
+ location: mockLocation({ query: { mode: CreateProjectModes.GitHub } })
+ });
+ expect(wrapper).toMatchSnapshot();
});
function shallowRender(props: Partial<CreateProjectPage['props']> = {}) {
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx
index 36203dcfc8e..4577ba4a0df 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx
@@ -24,16 +24,19 @@ import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import {
getGithubClientId,
getGithubOrganizations,
- getGithubRepositories
+ getGithubRepositories,
+ importGithubRepository
} from '../../../../api/alm-integrations';
import { mockGitHubRepository } from '../../../../helpers/mocks/alm-integrations';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
import GitHubProjectCreate from '../GitHubProjectCreate';
jest.mock('../../../../api/alm-integrations', () => ({
getGithubClientId: jest.fn().mockResolvedValue({ clientId: 'client-id-124' }),
getGithubOrganizations: jest.fn().mockResolvedValue({ organizations: [] }),
- getGithubRepositories: jest.fn().mockResolvedValue({ repositories: [], paging: {} })
+ getGithubRepositories: jest.fn().mockResolvedValue({ repositories: [], paging: {} }),
+ importGithubRepository: jest.fn().mockResolvedValue({ project: {} })
}));
const originalLocation = window.location;
@@ -61,7 +64,7 @@ beforeEach(() => {
});
it('should handle no settings', async () => {
- const wrapper = shallowRender({ settings: undefined });
+ const wrapper = shallowRender({ settings: [] });
await waitAndUpdate(wrapper);
expect(wrapper.state().error).toBe(true);
});
@@ -74,15 +77,41 @@ it('should redirect when no code', async () => {
expect(window.location.replace).toBeCalled();
});
+it('should redirect when no code - github.com', async () => {
+ const wrapper = shallowRender({
+ settings: [mockAlmSettingsInstance({ key: 'a', url: 'api.github.com' })]
+ });
+ await waitAndUpdate(wrapper);
+
+ expect(getGithubClientId).toBeCalled();
+ expect(window.location.replace).toBeCalledWith(
+ 'github.com/login/oauth/authorize?client_id=client-id-124&redirect_uri=http://localhost/projects/create?mode=github'
+ );
+});
+
+it('should not redirect when invalid clientId', async () => {
+ (getGithubClientId as jest.Mock).mockResolvedValue({ clientId: undefined });
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state().error).toBe(true);
+ expect(window.location.replace).not.toBeCalled();
+});
+
it('should fetch organizations when code', async () => {
const organizations = [
{ key: '1', name: 'org1' },
{ key: '2', name: 'org2' }
];
(getGithubOrganizations as jest.Mock).mockResolvedValueOnce({ organizations });
- const wrapper = shallowRender({ code: '123456' });
+ const replace = jest.fn();
+ const wrapper = shallowRender({
+ location: mockLocation({ query: { code: '123456' } }),
+ router: mockRouter({ replace })
+ });
await waitAndUpdate(wrapper);
+ expect(replace).toBeCalled();
expect(getGithubOrganizations).toBeCalled();
expect(wrapper.state().organizations).toBe(organizations);
});
@@ -98,7 +127,7 @@ it('should handle org selection', async () => {
repositories,
paging: { total: 1, pageIndex: 1 }
});
- const wrapper = shallowRender({ code: '123456' });
+ const wrapper = shallowRender({ location: mockLocation({ query: { code: '123456' } }) });
await waitAndUpdate(wrapper);
wrapper.instance().handleSelectOrganization('1');
@@ -154,7 +183,13 @@ it('should handle search', async () => {
await waitAndUpdate(wrapper);
- expect(getGithubRepositories).toBeCalledWith('a', 'o1', 1, query);
+ expect(getGithubRepositories).toBeCalledWith({
+ almSetting: 'a',
+ organization: 'o1',
+ p: 1,
+ ps: 30,
+ query: 'query'
+ });
expect(wrapper.state().repositories).toEqual(repositories);
});
@@ -169,11 +204,43 @@ it('should handle repository selection', async () => {
expect(wrapper.state().selectedRepository).toBe(repo);
});
+it('should handle importing', async () => {
+ const project = { key: 'new_project' };
+
+ (importGithubRepository as jest.Mock).mockResolvedValueOnce({ project });
+
+ const onProjectCreate = jest.fn();
+ const wrapper = shallowRender({ onProjectCreate });
+
+ wrapper.instance().handleImportRepository();
+ expect(importGithubRepository).not.toBeCalled();
+
+ const selectedOrganization = { key: 'org1', name: 'org1' };
+ const selectedRepository = mockGitHubRepository();
+ wrapper.setState({
+ selectedOrganization,
+ selectedRepository
+ });
+
+ wrapper.instance().handleImportRepository();
+ await waitAndUpdate(wrapper);
+ expect(importGithubRepository).toBeCalledWith(
+ 'a',
+ selectedOrganization.key,
+ selectedRepository.key
+ );
+ expect(onProjectCreate).toBeCalledWith([project.key]);
+});
+
function shallowRender(props: Partial<GitHubProjectCreate['props']> = {}) {
return shallow<GitHubProjectCreate>(
<GitHubProjectCreate
canAdmin={false}
- settings={mockAlmSettingsInstance({ key: 'a' })}
+ loadingBindings={false}
+ location={mockLocation()}
+ onProjectCreate={jest.fn()}
+ router={mockRouter()}
+ settings={[mockAlmSettingsInstance({ key: 'a', url: 'geh.company.com/api/v3' })]}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx
index b6a0ec8a26a..0bca0141708 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx
@@ -31,6 +31,7 @@ import GitHubProjectCreateRenderer, {
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ loadingBindings: true })).toMatchSnapshot('loading');
expect(shallowRender({ error: true })).toMatchSnapshot('error');
expect(shallowRender({ canAdmin: true, error: true })).toMatchSnapshot('error for admin');
@@ -64,11 +65,13 @@ it('should render correctly', () => {
});
describe('callback', () => {
+ const onImportRepository = jest.fn();
const onSelectOrganization = jest.fn();
const onSelectRepository = jest.fn();
const onSearch = jest.fn();
const org = { key: 'o1', name: 'org' };
const wrapper = shallowRender({
+ onImportRepository,
onSelectOrganization,
onSelectRepository,
onSearch,
@@ -83,7 +86,7 @@ describe('callback', () => {
it('should be called when org is selected', () => {
const value = 'o1';
- wrapper.find(SearchSelect).props().onSelect!({ value });
+ wrapper.find(SearchSelect).simulate('select', { value });
expect(onSelectOrganization).toBeCalledWith(value);
});
@@ -111,8 +114,11 @@ function shallowRender(props: Partial<GitHubProjectCreateRendererProps> = {}) {
<GitHubProjectCreateRenderer
canAdmin={false}
error={false}
- loading={false}
+ importing={false}
+ loadingBindings={false}
+ loadingOrganizations={false}
loadingRepositories={false}
+ onImportRepository={jest.fn()}
onLoadMore={jest.fn()}
onSearch={jest.fn()}
onSelectOrganization={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap
index a8e54b76f9a..31df0b466dc 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap
@@ -8,6 +8,7 @@ exports[`should render correctly 1`] = `
"key": "foo",
}
}
+ canAdmin={false}
importing={false}
loading={true}
onImportRepository={[Function]}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
index a7b160cc0bd..3a1c1edc18b 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
@@ -68,8 +68,9 @@ exports[`should render correctly if the BBS method is selected 1`] = `
className="page page-limited huge-spacer-bottom position-relative"
id="create-project"
>
- <Connect(BitbucketProjectCreate)
+ <BitbucketProjectCreate
bitbucketSettings={Array []}
+ canAdmin={false}
loadingBindings={true}
location={
Object {
@@ -107,6 +108,35 @@ exports[`should render correctly if the GitHub method is selected 1`] = `
>
<GitHubProjectCreate
canAdmin={false}
+ loadingBindings={true}
+ location={
+ Object {
+ "action": "PUSH",
+ "hash": "",
+ "key": "key",
+ "pathname": "/path",
+ "query": Object {
+ "mode": "github",
+ },
+ "search": "",
+ "state": Object {},
+ }
+ }
+ onProjectCreate={[Function]}
+ router={
+ Object {
+ "createHref": [MockFunction],
+ "createPath": [MockFunction],
+ "go": [MockFunction],
+ "goBack": [MockFunction],
+ "goForward": [MockFunction],
+ "isActive": [MockFunction],
+ "push": [MockFunction],
+ "replace": [MockFunction],
+ "setRouteLeaveHook": [MockFunction],
+ }
+ }
+ settings={Array []}
/>
</div>
</Fragment>
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap
index c97790e1755..f5deb0c4b9b 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap
@@ -128,9 +128,33 @@ exports[`should render correctly: error for admin 1`] = `
</div>
`;
+exports[`should render correctly: loading 1`] = `
+<DeferredSpinner
+ timeout={100}
+/>
+`;
+
exports[`should render correctly: no repositories 1`] = `
<div>
<CreateProjectPageHeader
+ additionalActions={
+ <div
+ className="display-flex-center pull-right"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ timeout={100}
+ />
+ <Button
+ className="button-large button-primary"
+ disabled={true}
+ onClick={[MockFunction]}
+ >
+ onboarding.create_project.import_selected_repo
+ </Button>
+ </div>
+ }
title={
<span
className="text-middle display-flex-center"
@@ -235,6 +259,24 @@ exports[`should render correctly: organizations 1`] = `
exports[`should render correctly: repositories 1`] = `
<div>
<CreateProjectPageHeader
+ additionalActions={
+ <div
+ className="display-flex-center pull-right"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ timeout={100}
+ />
+ <Button
+ className="button-large button-primary"
+ disabled={false}
+ onClick={[MockFunction]}
+ >
+ onboarding.create_project.import_selected_repo
+ </Button>
+ </div>
+ }
title={
<span
className="text-middle display-flex-center"
@@ -299,7 +341,7 @@ exports[`should render correctly: repositories 1`] = `
</div>
<Radio
checked={false}
- className="spacer-top spacer-bottom padded github-repository"
+ className="spacer-top spacer-bottom padded create-project-github-repository"
disabled={false}
key="repo1"
onCheck={[MockFunction]}
@@ -310,7 +352,7 @@ exports[`should render correctly: repositories 1`] = `
title="repository 1"
>
<div
- className="overflow-hidden text-ellipsis"
+ className="text-ellipsis"
>
repository 1
</div>
@@ -318,7 +360,7 @@ exports[`should render correctly: repositories 1`] = `
</Radio>
<Radio
checked={true}
- className="spacer-top spacer-bottom padded github-repository"
+ className="spacer-top spacer-bottom padded create-project-github-repository"
disabled={true}
key="repo2"
onCheck={[MockFunction]}
@@ -329,14 +371,14 @@ exports[`should render correctly: repositories 1`] = `
title="repository 1"
>
<div
- className="overflow-hidden text-ellipsis"
+ className="text-ellipsis"
>
repository 1
</div>
<em
className="notice text-muted-2 small display-flex-center"
>
- onboarding.create_project.already_imported
+ onboarding.create_project.repository_imported
<CheckIcon
className="little-spacer-left"
size={12}
@@ -346,7 +388,7 @@ exports[`should render correctly: repositories 1`] = `
</Radio>
<Radio
checked={true}
- className="spacer-top spacer-bottom padded github-repository"
+ className="spacer-top spacer-bottom padded create-project-github-repository"
disabled={false}
key="repo3"
onCheck={[MockFunction]}
@@ -357,7 +399,7 @@ exports[`should render correctly: repositories 1`] = `
title="repository 1"
>
<div
- className="overflow-hidden text-ellipsis"
+ className="text-ellipsis"
>
repository 1
</div>