aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/api/alm-integrations.ts17
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx109
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx103
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx63
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectAccordion-test.tsx105
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx59
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap78
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap55
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap13
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts18
-rw-r--r--server/sonar-web/src/main/js/types/alm-integration.ts11
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties2
19 files changed, 736 insertions, 16 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 e013a3721df..3824f97bb2c 100644
--- a/server/sonar-web/src/main/js/api/alm-integrations.ts
+++ b/server/sonar-web/src/main/js/api/alm-integrations.ts
@@ -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[] }> {
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
new file mode 100644
index 00000000000..20c4e079243
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx
index 292abc2e548..7cd34c6a42a 100644
--- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx
index 2d43a8bb26a..70a16ef8113 100644
--- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx
@@ -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}
+ />
))}
</>
);
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 c6f34ede827..f61257b5711 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
@@ -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>
);
}
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 a2cbadb2912..60c5abe3ce4 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
@@ -130,6 +130,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
loadingBindings={loading}
location={location}
onProjectCreate={this.handleProjectCreate}
+ router={router}
settings={azureSettings}
/>
);
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
new file mode 100644
index 00000000000..b9e7f3012b6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectAccordion-test.tsx
@@ -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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx
index 7ce89327f6e..c40406179e0 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx
@@ -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}
/>
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx
index d38e64d922e..9e09b0a1810 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx
new file mode 100644
index 00000000000..7ee191fc84a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx
@@ -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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap
new file mode 100644
index 00000000000..0d0e57cdbde
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap
index 47f70559c9f..40a022b1e4c 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap
@@ -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",
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap
index dc15c4a504a..dee9ad781a1 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap
@@ -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>
`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap
new file mode 100644
index 00000000000..8711c8e646b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap
@@ -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>
+`;
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 7f37890764c..970ce4d6c59 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
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap
index 99c2341cba2..48b2949cad4 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap
@@ -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>
}
diff --git a/server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts b/server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
index 1fdd37225e6..1196150f464 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
@@ -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,
diff --git a/server/sonar-web/src/main/js/types/alm-integration.ts b/server/sonar-web/src/main/js/types/alm-integration.ts
index cf70f73017a..fca78f50149 100644
--- a/server/sonar-web/src/main/js/types/alm-integration.ts
+++ b/server/sonar-web/src/main/js/types/alm-integration.ts
@@ -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;
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 02ee1bf0c49..36cda900b13 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -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