From dac19c9b3b12f880f71ca1d656a0c4c4710a6dbc Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Tue, 17 Nov 2020 17:17:26 +0100 Subject: [PATCH] SONAR-14059 Enable import of Azure repositories --- .../src/main/js/api/alm-integrations.ts | 12 +++++ .../create/project/AzureProjectAccordion.tsx | 22 +++++--- .../create/project/AzureProjectCreate.tsx | 39 ++++++++++++++ .../project/AzureProjectCreateRenderer.tsx | 25 ++++++++- .../apps/create/project/AzureProjectsList.tsx | 15 +++++- .../__tests__/AzureProjectAccordion-test.tsx | 5 ++ .../__tests__/AzureProjectCreate-test.tsx | 41 +++++++++++++- .../AzureProjectCreateRenderer-test.tsx | 3 ++ .../__tests__/AzureProjectsList-test.tsx | 2 + .../AzureProjectAccordion-test.tsx.snap | 50 +++++++++++++++-- .../AzureProjectCreate-test.tsx.snap | 3 ++ .../AzureProjectCreateRenderer-test.tsx.snap | 54 +++++++++++++++++++ .../AzureProjectsList-test.tsx.snap | 4 ++ 13 files changed, 263 insertions(+), 12 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 7aed2350b28..dc780b2fdef 100644 --- a/server/sonar-web/src/main/js/api/alm-integrations.ts +++ b/server/sonar-web/src/main/js/api/alm-integrations.ts @@ -70,6 +70,18 @@ export function searchAzureRepositories( ); } +export function importAzureRepository( + almSetting: string, + projectName: string, + repositoryName: string +): Promise<{ project: ProjectBase }> { + return postJSON('/api/alm_integrations/import_azure_project', { + almSetting, + projectName, + repositoryName + }).catch(throwGlobalError); +} + export function getBitbucketServerProjects( almSetting: string ): Promise<{ projects: BitbucketProject[] }> { diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx index 20c4e079243..87d2f5a1d8f 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx @@ -23,6 +23,7 @@ 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 Radio from 'sonar-ui-common/components/controls/Radio'; 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'; @@ -30,17 +31,20 @@ import { AzureProject, AzureRepository } from '../../../types/alm-integration'; import { CreateProjectModes } from './types'; export interface AzureProjectAccordionProps { + importing: boolean; loading: boolean; onOpen: (key: string) => void; - startsOpen: boolean; + onSelectRepository: (repository: AzureRepository) => void; project: AzureProject; repositories?: AzureRepository[]; + selectedRepository?: AzureRepository; + startsOpen: boolean; } const PAGE_SIZE = 30; export default function AzureProjectAccordion(props: AzureProjectAccordionProps) { - const { loading, startsOpen, project, repositories = [] } = props; + const { importing, loading, startsOpen, project, repositories = [], selectedRepository } = props; const [open, setOpen] = React.useState(startsOpen); const handleClick = () => { @@ -86,13 +90,19 @@ export default function AzureProjectAccordion(props: AzureProjectAccordionProps) <>
{limitedRepositories.map(repo => ( -
+ !importing && props.onSelectRepository(repo)} + value={repo.name}> {repo.name} -
+ ))}
{ } interface State { + importing: boolean; loading: boolean; loadingRepositories: T.Dict; patIsValid?: boolean; @@ -46,6 +48,7 @@ interface State { repositories: T.Dict; searching?: boolean; searchResults?: T.Dict; + selectedRepository?: AzureRepository; settings?: AlmSettingsInstance; submittingToken?: boolean; tokenValidationFailed: boolean; @@ -60,6 +63,7 @@ export default class AzureProjectCreate extends React.PureComponent { + const { selectedRepository, settings } = this.state; + + if (!settings || !selectedRepository) { + return; + } + + this.setState({ importing: true }); + + const createdProject = await importAzureRepository( + settings.key, + selectedRepository.projectName, + selectedRepository.name + ) + .then(({ project }) => project) + .catch(() => undefined); + + if (this.mounted) { + this.setState({ importing: false }); + if (createdProject) { + this.props.onProjectCreate([createdProject.key]); + } + } + }; + + handleSelectRepository = (selectedRepository: AzureRepository) => { + this.setState({ selectedRepository }); + }; + checkPersonalAccessToken = () => { const { settings } = this.state; @@ -236,6 +269,7 @@ export default class AzureProjectCreate extends React.PureComponent; + onImportRepository: () => void; onOpenProject: (key: string) => void; onPersonalAccessTokenCreate: (token: string) => void; onSearch: (query: string) => void; + onSelectRepository: (repository: AzureRepository) => void; projects?: AzureProject[]; repositories: T.Dict; searching?: boolean; searchResults?: T.Dict; + selectedRepository?: AzureRepository; settings?: AlmSettingsInstance; showPersonalAccessTokenForm?: boolean; submittingToken?: boolean; @@ -49,14 +54,16 @@ export interface AzureProjectCreateRendererProps { export default function AzureProjectCreateRenderer(props: AzureProjectCreateRendererProps) { const { canAdmin, + importing, loading, loadingRepositories, projects, repositories, searching, searchResults, - showPersonalAccessTokenForm, + selectedRepository, settings, + showPersonalAccessTokenForm, submittingToken, tokenValidationFailed } = props; @@ -64,6 +71,19 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend return ( <> + + + + ) + } title={ diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx index 344e3e204f0..86e109f3326 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx @@ -28,17 +28,27 @@ import AzureProjectAccordion from './AzureProjectAccordion'; import { CreateProjectModes } from './types'; export interface AzureProjectsListProps { + importing: boolean; loadingRepositories: T.Dict; onOpenProject: (key: string) => void; + onSelectRepository: (repository: AzureRepository) => void; projects?: AzureProject[]; repositories: T.Dict; searchResults?: T.Dict; + selectedRepository?: AzureRepository; } const PAGE_SIZE = 10; export default function AzureProjectsList(props: AzureProjectsListProps) { - const { loadingRepositories, projects = [], repositories, searchResults } = props; + const { + importing, + loadingRepositories, + projects = [], + repositories, + searchResults, + selectedRepository + } = props; const [page, setPage] = React.useState(1); @@ -83,10 +93,13 @@ export default function AzureProjectsList(props: AzureProjectsListProps) { {displayedProjects.map((p, i) => ( ))} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectAccordion-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectAccordion-test.tsx index b9e7f3012b6..394205a7852 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectAccordion-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectAccordion-test.tsx @@ -30,6 +30,9 @@ it('should render correctly', () => { expect(shallowRender({ repositories: [mockAzureRepository()] })).toMatchSnapshot( 'with a repository' ); + expect(shallowRender({ importing: true, repositories: [mockAzureRepository()] })).toMatchSnapshot( + 'importing' + ); }); it('should open when clicked', () => { @@ -95,7 +98,9 @@ it('should close when clicked', () => { function shallowRender(overrides: Partial = {}) { return shallow( { setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null), getAzureProjects: jest.fn().mockResolvedValue({ projects: [] }), getAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] }), - searchAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] }) + searchAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] }), + importAzureRepository: jest.fn().mockResolvedValue({ project: { key: 'baz' } }) }; }); @@ -169,6 +171,43 @@ it('should handle searching for repositories', async () => { expect(wrapper.state().searchResults).toBeUndefined(); }); +it('should select and import a repository', async () => { + const onProjectCreate = jest.fn(); + const repository = mockAzureRepository(); + const wrapper = shallowRender({ onProjectCreate }); + await waitAndUpdate(wrapper); + + expect(wrapper.state().selectedRepository).toBeUndefined(); + wrapper.instance().handleSelectRepository(repository); + expect(wrapper.state().selectedRepository).toBe(repository); + + wrapper.instance().handleImportRepository(); + expect(wrapper.state().importing).toBe(true); + expect(importAzureRepository).toBeCalledWith('foo', repository.projectName, repository.name); + await waitAndUpdate(wrapper); + + expect(onProjectCreate).toBeCalledWith(['baz']); + expect(wrapper.state().importing).toBe(false); +}); + +it('should handle no settings', () => { + const wrapper = shallowRender({ settings: [] }); + + wrapper.instance().fetchAzureProjects(); + wrapper.instance().fetchAzureRepositories('whatever'); + wrapper.instance().handleSearchRepositories('query'); + wrapper.instance().handleImportRepository(); + wrapper.instance().checkPersonalAccessToken(); + wrapper.instance().handlePersonalAccessTokenCreate(''); + + expect(getAzureProjects).not.toBeCalled(); + expect(getAzureRepositories).not.toBeCalled(); + expect(searchAzureRepositories).not.toBeCalled(); + expect(importAzureRepository).not.toBeCalled(); + expect(checkPersonalAccessTokenIsValid).not.toBeCalled(); + expect(setAlmPersonalAccessToken).not.toBeCalled(); +}); + function shallowRender(overrides: Partial = {}) { return shallow( ) { return shallow( = {}) { return shallow( `; +exports[`should render correctly: importing 1`] = ` + + Azure Project + + } +> + +
+ + + Azure repo 1 + + +
+ +
+
+`; + exports[`should render correctly: loading 1`] = ` -
Azure repo 1 -
+ + + + + } title={ + + + + } title={ + + + + } title={