@@ -20,6 +20,8 @@ | |||
import { get, getJSON, post, postJSON } from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { | |||
AzureProject, | |||
AzureRepository, | |||
BitbucketProject, | |||
BitbucketRepository, | |||
GithubOrganization, | |||
@@ -44,6 +46,21 @@ export function checkPersonalAccessTokenIsValid(almSetting: string): Promise<boo | |||
}); | |||
} | |||
export function getAzureProjects(almSetting: string): Promise<{ projects: AzureProject[] }> { | |||
return getJSON('/api/alm_integrations/list_azure_projects', { almSetting }).catch( | |||
throwGlobalError | |||
); | |||
} | |||
export function getAzureRepositories( | |||
almSetting: string, | |||
projectName: string | |||
): Promise<{ repositories: AzureRepository[] }> { | |||
return getJSON('/api/alm_integrations/search_azure_repos', { almSetting, projectName }).catch( | |||
throwGlobalError | |||
); | |||
} | |||
export function getBitbucketServerProjects( | |||
almSetting: string | |||
): Promise<{ projects: BitbucketProject[] }> { |
@@ -0,0 +1,109 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import BoxedGroupAccordion from 'sonar-ui-common/components/controls/BoxedGroupAccordion'; | |||
import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { AzureProject, AzureRepository } from '../../../types/alm-integration'; | |||
import { CreateProjectModes } from './types'; | |||
export interface AzureProjectAccordionProps { | |||
loading: boolean; | |||
onOpen: (key: string) => void; | |||
startsOpen: boolean; | |||
project: AzureProject; | |||
repositories?: AzureRepository[]; | |||
} | |||
const PAGE_SIZE = 30; | |||
export default function AzureProjectAccordion(props: AzureProjectAccordionProps) { | |||
const { loading, startsOpen, project, repositories = [] } = props; | |||
const [open, setOpen] = React.useState(startsOpen); | |||
const handleClick = () => { | |||
if (!open) { | |||
props.onOpen(project.key); | |||
} | |||
setOpen(!open); | |||
}; | |||
const [page, setPage] = React.useState(1); | |||
const limitedRepositories = repositories.slice(0, page * PAGE_SIZE); | |||
return ( | |||
<BoxedGroupAccordion | |||
className={classNames('big-spacer-bottom', { | |||
open | |||
})} | |||
onClick={handleClick} | |||
open={open} | |||
title={<h3>{project.name}</h3>}> | |||
{open && ( | |||
<DeferredSpinner loading={loading}> | |||
{/* The extra loading guard is to prevent the flash of the Alert */} | |||
{!loading && repositories.length === 0 ? ( | |||
<Alert variant="warning"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.create_project.azure.no_repositories')} | |||
id="onboarding.create_project.azure.no_repositories" | |||
values={{ | |||
link: ( | |||
<Link | |||
to={{ | |||
pathname: '/projects/create', | |||
query: { mode: CreateProjectModes.AzureDevOps, resetPat: 1 } | |||
}}> | |||
{translate('onboarding.create_project.update_your_token')} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
</Alert> | |||
) : ( | |||
<> | |||
<div className="display-flex-wrap"> | |||
{limitedRepositories.map(repo => ( | |||
<div | |||
className="abs-width-400 overflow-hidden spacer-top spacer-bottom" | |||
key={repo.name}> | |||
<strong className="text-ellipsis" title={repo.name}> | |||
{repo.name} | |||
</strong> | |||
</div> | |||
))} | |||
</div> | |||
<ListFooter | |||
count={limitedRepositories.length} | |||
total={repositories.length} | |||
loadMore={() => setPage(p => p + 1)} | |||
/> | |||
</> | |||
)} | |||
</DeferredSpinner> | |||
)} | |||
</BoxedGroupAccordion> | |||
); | |||
} |
@@ -21,12 +21,15 @@ import * as React from 'react'; | |||
import { WithRouterProps } from 'react-router'; | |||
import { | |||
checkPersonalAccessTokenIsValid, | |||
getAzureProjects, | |||
getAzureRepositories, | |||
setAlmPersonalAccessToken | |||
} from '../../../api/alm-integrations'; | |||
import { AzureProject, AzureRepository } from '../../../types/alm-integration'; | |||
import { AlmSettingsInstance } from '../../../types/alm-settings'; | |||
import AzureCreateProjectRenderer from './AzureProjectCreateRenderer'; | |||
interface Props extends Pick<WithRouterProps, 'location'> { | |||
interface Props extends Pick<WithRouterProps, 'location' | 'router'> { | |||
canAdmin: boolean; | |||
loadingBindings: boolean; | |||
onProjectCreate: (projectKeys: string[]) => void; | |||
@@ -35,7 +38,10 @@ interface Props extends Pick<WithRouterProps, 'location'> { | |||
interface State { | |||
loading: boolean; | |||
loadingRepositories: T.Dict<boolean>; | |||
patIsValid?: boolean; | |||
projects?: AzureProject[]; | |||
repositories: T.Dict<AzureRepository[]>; | |||
settings?: AlmSettingsInstance; | |||
submittingToken?: boolean; | |||
tokenValidationFailed: boolean; | |||
@@ -51,6 +57,8 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State | |||
// one from the list. | |||
settings: props.settings[0], | |||
loading: false, | |||
loadingRepositories: {}, | |||
repositories: {}, | |||
tokenValidationFailed: false | |||
}; | |||
} | |||
@@ -78,14 +86,84 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State | |||
const patIsValid = await this.checkPersonalAccessToken().catch(() => false); | |||
let projects: AzureProject[] | undefined; | |||
if (patIsValid) { | |||
projects = await this.fetchAzureProjects(); | |||
} | |||
const { repositories } = this.state; | |||
let firstProjectKey: string; | |||
if (projects && projects.length > 0) { | |||
firstProjectKey = projects[0].key; | |||
this.setState(({ loadingRepositories }) => ({ | |||
loadingRepositories: { ...loadingRepositories, [firstProjectKey]: true } | |||
})); | |||
const repos = await this.fetchAzureRepositories(firstProjectKey); | |||
repositories[firstProjectKey] = repos; | |||
} | |||
if (this.mounted) { | |||
this.setState({ | |||
patIsValid, | |||
loading: false | |||
this.setState(({ loadingRepositories }) => { | |||
if (firstProjectKey) { | |||
loadingRepositories[firstProjectKey] = false; | |||
} | |||
return { | |||
patIsValid, | |||
loading: false, | |||
loadingRepositories: { ...loadingRepositories }, | |||
projects, | |||
repositories | |||
}; | |||
}); | |||
} | |||
}; | |||
fetchAzureProjects = (): Promise<AzureProject[] | undefined> => { | |||
const { settings } = this.state; | |||
if (!settings) { | |||
return Promise.resolve(undefined); | |||
} | |||
return getAzureProjects(settings.key).then(({ projects }) => projects); | |||
}; | |||
fetchAzureRepositories = (projectKey: string): Promise<AzureRepository[]> => { | |||
const { settings } = this.state; | |||
if (!settings) { | |||
return Promise.resolve([]); | |||
} | |||
return getAzureRepositories(settings.key, projectKey) | |||
.then(({ repositories }) => repositories) | |||
.catch(() => []); | |||
}; | |||
cleanUrl = () => { | |||
const { location, router } = this.props; | |||
delete location.query.resetPat; | |||
router.replace(location); | |||
}; | |||
handleOpenProject = async (projectKey: string) => { | |||
this.setState(({ loadingRepositories }) => ({ | |||
loadingRepositories: { ...loadingRepositories, [projectKey]: true } | |||
})); | |||
const projectRepos = await this.fetchAzureRepositories(projectKey); | |||
this.setState(({ loadingRepositories, repositories }) => ({ | |||
loadingRepositories: { ...loadingRepositories, [projectKey]: false }, | |||
repositories: { ...repositories, [projectKey]: projectRepos } | |||
})); | |||
}; | |||
checkPersonalAccessToken = () => { | |||
const { settings } = this.state; | |||
@@ -114,7 +192,7 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State | |||
if (patIsValid) { | |||
this.cleanUrl(); | |||
await this.fetchInitialData(); | |||
this.fetchInitialData(); | |||
} | |||
} | |||
} catch (e) { | |||
@@ -126,13 +204,26 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State | |||
render() { | |||
const { canAdmin, loadingBindings, location } = this.props; | |||
const { loading, patIsValid, settings, submittingToken, tokenValidationFailed } = this.state; | |||
const { | |||
loading, | |||
loadingRepositories, | |||
patIsValid, | |||
projects, | |||
repositories, | |||
settings, | |||
submittingToken, | |||
tokenValidationFailed | |||
} = this.state; | |||
return ( | |||
<AzureCreateProjectRenderer | |||
canAdmin={canAdmin} | |||
loading={loading || loadingBindings} | |||
loadingRepositories={loadingRepositories} | |||
onOpenProject={this.handleOpenProject} | |||
onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate} | |||
projects={projects} | |||
repositories={repositories} | |||
settings={settings} | |||
showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)} | |||
submittingToken={submittingToken} |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getBaseUrl } from 'sonar-ui-common/helpers/urls'; | |||
import { AzureProject, AzureRepository } from '../../../types/alm-integration'; | |||
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; | |||
import AzurePersonalAccessTokenForm from './AzurePersonalAccessTokenForm'; | |||
import AzureProjectsList from './AzureProjectsList'; | |||
@@ -29,7 +30,11 @@ import WrongBindingCountAlert from './WrongBindingCountAlert'; | |||
export interface AzureProjectCreateRendererProps { | |||
canAdmin?: boolean; | |||
loading: boolean; | |||
loadingRepositories: T.Dict<boolean>; | |||
onOpenProject: (key: string) => void; | |||
onPersonalAccessTokenCreate: (token: string) => void; | |||
projects?: AzureProject[]; | |||
repositories: T.Dict<AzureRepository[]>; | |||
settings?: AlmSettingsInstance; | |||
showPersonalAccessTokenForm?: boolean; | |||
submittingToken?: boolean; | |||
@@ -40,6 +45,9 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend | |||
const { | |||
canAdmin, | |||
loading, | |||
loadingRepositories, | |||
projects, | |||
repositories, | |||
showPersonalAccessTokenForm, | |||
settings, | |||
submittingToken, | |||
@@ -80,7 +88,12 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend | |||
/> | |||
</div> | |||
) : ( | |||
<AzureProjectsList /> | |||
<AzureProjectsList | |||
loadingRepositories={loadingRepositories} | |||
onOpenProject={props.onOpenProject} | |||
projects={projects} | |||
repositories={repositories} | |||
/> | |||
))} | |||
</> | |||
); |
@@ -18,14 +18,71 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { AzureProject, AzureRepository } from '../../../types/alm-integration'; | |||
import AzureProjectAccordion from './AzureProjectAccordion'; | |||
import { CreateProjectModes } from './types'; | |||
export interface AzureProjectsListProps {} | |||
export interface AzureProjectsListProps { | |||
loadingRepositories: T.Dict<boolean>; | |||
onOpenProject: (key: string) => void; | |||
projects?: AzureProject[]; | |||
repositories: T.Dict<AzureRepository[]>; | |||
} | |||
const PAGE_SIZE = 10; | |||
export default function AzureProjectsList(props: AzureProjectsListProps) { | |||
const { loadingRepositories, projects = [], repositories } = props; | |||
const [page, setPage] = React.useState(1); | |||
if (projects.length === 0) { | |||
return ( | |||
<Alert className="spacer-top" variant="warning"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.create_project.azure.no_projects')} | |||
id="onboarding.create_project.azure.no_projects" | |||
values={{ | |||
link: ( | |||
<Link | |||
to={{ | |||
pathname: '/projects/create', | |||
query: { mode: CreateProjectModes.AzureDevOps, resetPat: 1 } | |||
}}> | |||
{translate('onboarding.create_project.update_your_token')} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
</Alert> | |||
); | |||
} | |||
const filteredProjects = projects.slice(0, page * PAGE_SIZE); | |||
export default function AzureProjectsList(_props: AzureProjectsListProps) { | |||
return ( | |||
<div> | |||
<Alert variant="warning">Coming soon!</Alert> | |||
{filteredProjects.map((p, i) => ( | |||
<AzureProjectAccordion | |||
key={p.key} | |||
loading={Boolean(loadingRepositories[p.key])} | |||
onOpen={props.onOpenProject} | |||
project={p} | |||
repositories={repositories[p.key]} | |||
startsOpen={i === 0} | |||
/> | |||
))} | |||
<ListFooter | |||
count={filteredProjects.length} | |||
loadMore={() => setPage(p => p + 1)} | |||
total={projects.length} | |||
/> | |||
</div> | |||
); | |||
} |
@@ -130,6 +130,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> { | |||
loadingBindings={loading} | |||
location={location} | |||
onProjectCreate={this.handleProjectCreate} | |||
router={router} | |||
settings={azureSettings} | |||
/> | |||
); |
@@ -0,0 +1,105 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import BoxedGroupAccordion from 'sonar-ui-common/components/controls/BoxedGroupAccordion'; | |||
import { mockAzureProject, mockAzureRepository } from '../../../../helpers/mocks/alm-integrations'; | |||
import AzureProjectAccordion, { AzureProjectAccordionProps } from '../AzureProjectAccordion'; | |||
it('should render correctly', () => { | |||
expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); | |||
expect(shallowRender({ startsOpen: false })).toMatchSnapshot('closed'); | |||
expect(shallowRender({ repositories: [mockAzureRepository()] })).toMatchSnapshot( | |||
'with a repository' | |||
); | |||
}); | |||
it('should open when clicked', () => { | |||
const onOpen = jest.fn(); | |||
const wrapper = shallowRender({ | |||
onOpen, | |||
repositories: [mockAzureRepository()], | |||
startsOpen: false | |||
}); | |||
expect( | |||
wrapper | |||
.find(BoxedGroupAccordion) | |||
.children() | |||
.exists() | |||
).toBe(false); | |||
wrapper | |||
.find(BoxedGroupAccordion) | |||
.props() | |||
.onClick(); | |||
expect(onOpen).toBeCalled(); | |||
expect( | |||
wrapper | |||
.find(BoxedGroupAccordion) | |||
.children() | |||
.exists() | |||
).toBe(true); | |||
}); | |||
it('should close when clicked', () => { | |||
const onOpen = jest.fn(); | |||
const wrapper = shallowRender({ | |||
onOpen, | |||
repositories: [mockAzureRepository()] | |||
}); | |||
expect( | |||
wrapper | |||
.find(BoxedGroupAccordion) | |||
.children() | |||
.exists() | |||
).toBe(true); | |||
wrapper | |||
.find(BoxedGroupAccordion) | |||
.props() | |||
.onClick(); | |||
expect(onOpen).not.toBeCalled(); | |||
expect( | |||
wrapper | |||
.find(BoxedGroupAccordion) | |||
.children() | |||
.exists() | |||
).toBe(false); | |||
}); | |||
function shallowRender(overrides: Partial<AzureProjectAccordionProps> = {}) { | |||
return shallow( | |||
<AzureProjectAccordion | |||
loading={false} | |||
onOpen={jest.fn()} | |||
project={mockAzureProject()} | |||
startsOpen={true} | |||
{...overrides} | |||
/> | |||
); | |||
} |
@@ -23,17 +23,22 @@ import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { | |||
checkPersonalAccessTokenIsValid, | |||
getAzureProjects, | |||
getAzureRepositories, | |||
setAlmPersonalAccessToken | |||
} from '../../../../api/alm-integrations'; | |||
import { mockAzureProject, mockAzureRepository } from '../../../../helpers/mocks/alm-integrations'; | |||
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; | |||
import { mockLocation } from '../../../../helpers/testMocks'; | |||
import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
import AzureProjectCreate from '../AzureProjectCreate'; | |||
jest.mock('../../../../api/alm-integrations', () => { | |||
return { | |||
checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue(true), | |||
setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null) | |||
setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null), | |||
getAzureProjects: jest.fn().mockResolvedValue({ projects: [] }), | |||
getAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] }) | |||
}; | |||
}); | |||
@@ -66,7 +71,8 @@ it('should correctly handle an invalid PAT', async () => { | |||
}); | |||
it('should correctly handle setting a new PAT', async () => { | |||
const wrapper = shallowRender(); | |||
const router = mockRouter(); | |||
const wrapper = shallowRender({ router }); | |||
wrapper.instance().handlePersonalAccessTokenCreate('token'); | |||
expect(setAlmPersonalAccessToken).toBeCalledWith('foo', 'token'); | |||
expect(wrapper.state().submittingToken).toBe(true); | |||
@@ -76,6 +82,59 @@ it('should correctly handle setting a new PAT', async () => { | |||
expect(checkPersonalAccessTokenIsValid).toBeCalled(); | |||
expect(wrapper.state().submittingToken).toBe(false); | |||
expect(wrapper.state().tokenValidationFailed).toBe(true); | |||
// Try again, this time with a correct token: | |||
wrapper.instance().handlePersonalAccessTokenCreate('correct token'); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().tokenValidationFailed).toBe(false); | |||
expect(router.replace).toBeCalled(); | |||
}); | |||
it('should correctly fetch projects and repositories on mount', async () => { | |||
const project = mockAzureProject(); | |||
(getAzureProjects as jest.Mock).mockResolvedValueOnce({ projects: [project] }); | |||
(getAzureRepositories as jest.Mock).mockResolvedValueOnce({ | |||
repositories: [mockAzureRepository()] | |||
}); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(getAzureProjects).toBeCalled(); | |||
expect(getAzureRepositories).toBeCalledTimes(1); | |||
expect(getAzureRepositories).toBeCalledWith('foo', project.key); | |||
}); | |||
it('should handle opening a project', async () => { | |||
const projects = [ | |||
mockAzureProject(), | |||
mockAzureProject({ key: 'project2', name: 'Project to open' }) | |||
]; | |||
const firstProjectRepos = [mockAzureRepository()]; | |||
const secondProjectRepos = [mockAzureRepository({ projectName: projects[1].name })]; | |||
(getAzureProjects as jest.Mock).mockResolvedValueOnce({ projects }); | |||
(getAzureRepositories as jest.Mock) | |||
.mockResolvedValueOnce({ | |||
repositories: firstProjectRepos | |||
}) | |||
.mockResolvedValueOnce({ | |||
repositories: secondProjectRepos | |||
}); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleOpenProject(projects[1].key); | |||
await waitAndUpdate(wrapper); | |||
expect(getAzureRepositories).toBeCalledWith('foo', projects[1].key); | |||
expect(wrapper.state().repositories).toEqual({ | |||
[projects[0].key]: firstProjectRepos, | |||
[projects[1].key]: secondProjectRepos | |||
}); | |||
}); | |||
function shallowRender(overrides: Partial<AzureProjectCreate['props']> = {}) { | |||
@@ -85,6 +144,7 @@ function shallowRender(overrides: Partial<AzureProjectCreate['props']> = {}) { | |||
loadingBindings={false} | |||
location={mockLocation()} | |||
onProjectCreate={jest.fn()} | |||
router={mockRouter()} | |||
settings={[mockAlmSettingsInstance({ alm: AlmKeys.Azure, key: 'foo' })]} | |||
{...overrides} | |||
/> |
@@ -20,6 +20,7 @@ | |||
/* eslint-disable sonarjs/no-duplicate-string */ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockAzureProject, mockAzureRepository } from '../../../../helpers/mocks/alm-integrations'; | |||
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
import AzureProjectCreateRenderer, { | |||
@@ -34,11 +35,17 @@ it('should render correctly', () => { | |||
}); | |||
function shallowRender(overrides: Partial<AzureProjectCreateRendererProps>) { | |||
const project = mockAzureProject(); | |||
return shallow( | |||
<AzureProjectCreateRenderer | |||
canAdmin={true} | |||
loading={false} | |||
loadingRepositories={{}} | |||
onOpenProject={jest.fn()} | |||
onPersonalAccessTokenCreate={jest.fn()} | |||
projects={[project]} | |||
repositories={{ [project.key]: [mockAzureRepository()] }} | |||
tokenValidationFailed={false} | |||
settings={mockAlmSettingsInstance({ alm: AlmKeys.Azure })} | |||
showPersonalAccessTokenForm={false} |
@@ -0,0 +1,59 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | |||
import { mockAzureProject } from '../../../../helpers/mocks/alm-integrations'; | |||
import AzureProjectAccordion from '../AzureProjectAccordion'; | |||
import AzureProjectsList, { AzureProjectsListProps } from '../AzureProjectsList'; | |||
it('should render correctly', () => { | |||
expect(shallowRender({})).toMatchSnapshot('default'); | |||
expect(shallowRender({ projects: [] })).toMatchSnapshot('empty'); | |||
}); | |||
it('should handle pagination', () => { | |||
const projects = new Array(21) | |||
.fill(1) | |||
.map((_, i) => mockAzureProject({ key: `project-${i}`, name: `Project #${i}` })); | |||
const wrapper = shallowRender({ projects }); | |||
expect(wrapper.find(AzureProjectAccordion)).toHaveLength(10); | |||
wrapper.find(ListFooter).props().loadMore!(); | |||
expect(wrapper.find(AzureProjectAccordion)).toHaveLength(20); | |||
}); | |||
function shallowRender(overrides: Partial<AzureProjectsListProps> = {}) { | |||
const project = mockAzureProject(); | |||
return shallow( | |||
<AzureProjectsList | |||
loadingRepositories={{}} | |||
onOpenProject={jest.fn()} | |||
projects={[project]} | |||
repositories={{ [project.key]: [] }} | |||
{...overrides} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,78 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: closed 1`] = ` | |||
<BoxedGroupAccordion | |||
className="big-spacer-bottom" | |||
onClick={[Function]} | |||
open={false} | |||
title={ | |||
<h3> | |||
Azure Project | |||
</h3> | |||
} | |||
/> | |||
`; | |||
exports[`should render correctly: loading 1`] = ` | |||
<BoxedGroupAccordion | |||
className="big-spacer-bottom open" | |||
onClick={[Function]} | |||
open={true} | |||
title={ | |||
<h3> | |||
Azure Project | |||
</h3> | |||
} | |||
> | |||
<DeferredSpinner | |||
loading={true} | |||
> | |||
<div | |||
className="display-flex-wrap" | |||
/> | |||
<ListFooter | |||
count={0} | |||
loadMore={[Function]} | |||
total={0} | |||
/> | |||
</DeferredSpinner> | |||
</BoxedGroupAccordion> | |||
`; | |||
exports[`should render correctly: with a repository 1`] = ` | |||
<BoxedGroupAccordion | |||
className="big-spacer-bottom open" | |||
onClick={[Function]} | |||
open={true} | |||
title={ | |||
<h3> | |||
Azure Project | |||
</h3> | |||
} | |||
> | |||
<DeferredSpinner | |||
loading={false} | |||
> | |||
<div | |||
className="display-flex-wrap" | |||
> | |||
<div | |||
className="abs-width-400 overflow-hidden spacer-top spacer-bottom" | |||
key="Azure repo 1" | |||
> | |||
<strong | |||
className="text-ellipsis" | |||
title="Azure repo 1" | |||
> | |||
Azure repo 1 | |||
</strong> | |||
</div> | |||
</div> | |||
<ListFooter | |||
count={1} | |||
loadMore={[Function]} | |||
total={1} | |||
/> | |||
</DeferredSpinner> | |||
</BoxedGroupAccordion> | |||
`; |
@@ -4,7 +4,10 @@ exports[`should render correctly 1`] = ` | |||
<AzureProjectCreateRenderer | |||
canAdmin={true} | |||
loading={true} | |||
loadingRepositories={Object {}} | |||
onOpenProject={[Function]} | |||
onPersonalAccessTokenCreate={[Function]} | |||
repositories={Object {}} | |||
settings={ | |||
Object { | |||
"alm": "azure", |
@@ -64,7 +64,28 @@ exports[`should render correctly: project list 1`] = ` | |||
</span> | |||
} | |||
/> | |||
<AzureProjectsList /> | |||
<AzureProjectsList | |||
loadingRepositories={Object {}} | |||
onOpenProject={[MockFunction]} | |||
projects={ | |||
Array [ | |||
Object { | |||
"key": "azure-project-1", | |||
"name": "Azure Project", | |||
}, | |||
] | |||
} | |||
repositories={ | |||
Object { | |||
"azure-project-1": Array [ | |||
Object { | |||
"name": "Azure repo 1", | |||
"projectName": "Azure Project", | |||
}, | |||
], | |||
} | |||
} | |||
/> | |||
</Fragment> | |||
`; | |||
@@ -0,0 +1,55 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: default 1`] = ` | |||
<div> | |||
<AzureProjectAccordion | |||
key="azure-project-1" | |||
loading={false} | |||
onOpen={[MockFunction]} | |||
project={ | |||
Object { | |||
"key": "azure-project-1", | |||
"name": "Azure Project", | |||
} | |||
} | |||
repositories={Array []} | |||
startsOpen={true} | |||
/> | |||
<ListFooter | |||
count={1} | |||
loadMore={[Function]} | |||
total={1} | |||
/> | |||
</div> | |||
`; | |||
exports[`should render correctly: empty 1`] = ` | |||
<Alert | |||
className="spacer-top" | |||
variant="warning" | |||
> | |||
<FormattedMessage | |||
defaultMessage="onboarding.create_project.azure.no_projects" | |||
id="onboarding.create_project.azure.no_projects" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/projects/create", | |||
"query": Object { | |||
"mode": "azure", | |||
"resetPat": 1, | |||
}, | |||
} | |||
} | |||
> | |||
onboarding.create_project.update_your_token | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
`; |
@@ -85,6 +85,19 @@ exports[`should render correctly if the Azure method is selected 1`] = ` | |||
} | |||
} | |||
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> |
@@ -16,7 +16,7 @@ exports[`should render correctly: create 1`] = ` | |||
settings.almintegration.form.url.azure.help | |||
<br /> | |||
<em> | |||
https://ado.your-company.com/ | |||
https://ado.your-company.com/DefaultCollection | |||
</em> | |||
</React.Fragment> | |||
} | |||
@@ -53,7 +53,7 @@ exports[`should render correctly: edit 1`] = ` | |||
settings.almintegration.form.url.azure.help | |||
<br /> | |||
<em> | |||
https://ado.your-company.com/ | |||
https://ado.your-company.com/DefaultCollection | |||
</em> | |||
</React.Fragment> | |||
} |
@@ -18,12 +18,30 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { | |||
AzureProject, | |||
AzureRepository, | |||
BitbucketProject, | |||
BitbucketRepository, | |||
GithubRepository, | |||
GitlabProject | |||
} from '../../types/alm-integration'; | |||
export function mockAzureProject(overrides: Partial<AzureProject> = {}): AzureProject { | |||
return { | |||
key: 'azure-project-1', | |||
name: 'Azure Project', | |||
...overrides | |||
}; | |||
} | |||
export function mockAzureRepository(overrides: Partial<AzureRepository> = {}): AzureRepository { | |||
return { | |||
name: 'Azure repo 1', | |||
projectName: 'Azure Project', | |||
...overrides | |||
}; | |||
} | |||
export function mockBitbucketProject(overrides: Partial<BitbucketProject> = {}): BitbucketProject { | |||
return { | |||
id: 1, |
@@ -17,6 +17,17 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
export interface AzureProject { | |||
key: string; | |||
name: string; | |||
} | |||
export interface AzureRepository { | |||
name: string; | |||
projectName: string; | |||
} | |||
export interface BitbucketProject { | |||
id: number; | |||
key: string; |
@@ -3287,6 +3287,8 @@ onboarding.create_project.import_selected_repo=Set up selected repository | |||
onboarding.create_project.go_to_project=Go to project | |||
onboarding.create_project.azure.title=Which Azure DevOps Server repository do you want to set up? | |||
onboarding.create_project.azure.no_projects=No projects could be fetched from Azure DevOps Server. Contact your system administrator, or {link}. | |||
onboarding.create_project.azure.no_repositories=Could not fetch repositories for this project. Contact your system administrator, or {link}. | |||
onboarding.create_project.github.title=Which GitHub repository do you want to set up? | |||
onboarding.create_project.github.choose_organization=Choose organization | |||
onboarding.create_project.github.warning.title=Could not connect to GitHub |