diff options
author | Duarte Meneses <duarte.meneses@sonarsource.com> | 2020-06-29 08:22:22 -0500 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-07-01 20:05:53 +0000 |
commit | db5fc175198e954404be2a9d73d63b7e3c19af49 (patch) | |
tree | 537270857368638dc50152bddc9345f64516827c /server/sonar-web/src/main | |
parent | 46d5b7c51b9c41d35d90416281144385555b9059 (diff) | |
download | sonarqube-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')
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> |