瀏覽代碼

SONAR-14057 Display Azure Projects and Repositories

tags/8.6.0.39681
Jeremy Davis 3 年之前
父節點
當前提交
942a20cb4e
共有 19 個檔案被更改,包括 736 行新增16 行删除
  1. 17
    0
      server/sonar-web/src/main/js/api/alm-integrations.ts
  2. 109
    0
      server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx
  3. 97
    6
      server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx
  4. 14
    1
      server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx
  5. 60
    3
      server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx
  6. 1
    0
      server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
  7. 105
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectAccordion-test.tsx
  8. 63
    3
      server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx
  9. 7
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx
  10. 59
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectsList-test.tsx
  11. 78
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap
  12. 3
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap
  13. 22
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap
  14. 55
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectsList-test.tsx.snap
  15. 13
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
  16. 2
    2
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap
  17. 18
    0
      server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
  18. 11
    0
      server/sonar-web/src/main/js/types/alm-integration.ts
  19. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 17
- 0
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[] }> {

+ 109
- 0
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>
);
}

+ 97
- 6
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}

+ 14
- 1
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}
/>
))}
</>
);

+ 60
- 3
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>
);
}

+ 1
- 0
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}
/>
);

+ 105
- 0
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}
/>
);
}

+ 63
- 3
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}
/>

+ 7
- 0
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}

+ 59
- 0
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}
/>
);
}

+ 78
- 0
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>
`;

+ 3
- 0
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",

+ 22
- 1
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>
`;


+ 55
- 0
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>
`;

+ 13
- 0
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>

+ 2
- 2
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>
}

+ 18
- 0
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,

+ 11
- 0
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;

+ 2
- 0
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

Loading…
取消
儲存