diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2020-07-27 17:30:15 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-08-17 20:06:22 +0000 |
commit | 6e7ab27398db34433cc1202379a574bf4342c301 (patch) | |
tree | ef02a8d66e67fabe5e8c31413ef0eb82884a5513 /server | |
parent | 6adfaffba7811c4bbf1ede4d6e5c3c15e03862df (diff) | |
download | sonarqube-6e7ab27398db34433cc1202379a574bf4342c301.tar.gz sonarqube-6e7ab27398db34433cc1202379a574bf4342c301.zip |
SONAR-13629 Display gitlab projects
Diffstat (limited to 'server')
17 files changed, 848 insertions, 40 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 b0007b3c480..51914405af5 100644 --- a/server/sonar-web/src/main/js/api/alm-integrations.ts +++ b/server/sonar-web/src/main/js/api/alm-integrations.ts @@ -23,7 +23,8 @@ import { BitbucketProject, BitbucketRepository, GithubOrganization, - GithubRepository + GithubRepository, + GitlabProject } from '../types/alm-integration'; import { ProjectBase } from './components'; @@ -120,16 +121,33 @@ export function getGithubOrganizations( export function getGithubRepositories(data: { almSetting: string; organization: string; - ps: number; - p?: number; + pageSize: number; + page?: number; query?: string; }): Promise<{ repositories: GithubRepository[]; paging: T.Paging }> { - const { almSetting, organization, ps, p = 1, query } = data; + const { almSetting, organization, pageSize, page = 1, query } = data; return getJSON('/api/alm_integrations/list_github_repositories', { almSetting, organization, - p, - ps, + p: page, + ps: pageSize, q: query || undefined }).catch(throwGlobalError); } + +export function getGitlabProjects(data: { + almSetting: string; + page?: number; + pageSize?: number; + query?: string; +}): Promise<{ projects: GitlabProject[]; projectsPaging: T.Paging }> { + const { almSetting, pageSize, page, query } = data; + return getJSON('/api/alm_integrations/search_gitlab_repos', { + almSetting, + projectName: query || undefined, + p: page, + ps: pageSize + }) + .then(({ repositories, paging }) => ({ projects: repositories, projectsPaging: paging })) + .catch(throwGlobalError); +} 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 5b79dd82e6b..3976e1acb93 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 @@ -138,6 +138,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> { loadingBindings={loading} location={location} onProjectCreate={this.handleProjectCreate} + router={router} settings={gitlabSettings} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx index 1c03f8c3b50..f438d2486d5 100644 --- a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx @@ -176,8 +176,8 @@ export default class GitHubProjectCreate extends React.Component<Props, State> { const data = await getGithubRepositories({ almSetting: settings.key, organization: organizationKey, - ps: REPOSITORY_PAGE_SIZE, - p: page, + pageSize: REPOSITORY_PAGE_SIZE, + page, query }); diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx index 33d05358658..d7f2ad37283 100644 --- a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx @@ -21,12 +21,14 @@ import * as React from 'react'; import { WithRouterProps } from 'react-router'; import { checkPersonalAccessTokenIsValid, + getGitlabProjects, setAlmPersonalAccessToken } from '../../../api/alm-integrations'; +import { GitlabProject } from '../../../types/alm-integration'; import { AlmSettingsInstance } from '../../../types/alm-settings'; import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer'; -interface Props extends Pick<WithRouterProps, 'location'> { +interface Props extends Pick<WithRouterProps, 'location' | 'router'> { canAdmin: boolean; loadingBindings: boolean; onProjectCreate: (projectKeys: string[]) => void; @@ -35,20 +37,32 @@ interface Props extends Pick<WithRouterProps, 'location'> { interface State { loading: boolean; + loadingMore: boolean; + projects?: GitlabProject[]; + projectsPaging: T.Paging; submittingToken: boolean; tokenIsValid: boolean; tokenValidationFailed: boolean; + searching: boolean; + searchQuery: string; settings?: AlmSettingsInstance; } +const GITLAB_PROJECTS_PAGESIZE = 30; + export default class GitlabProjectCreate extends React.PureComponent<Props, State> { mounted = false; constructor(props: Props) { super(props); + this.state = { loading: false, + loadingMore: false, + projectsPaging: { pageIndex: 1, total: 0, pageSize: GITLAB_PROJECTS_PAGESIZE }, tokenIsValid: false, + searching: false, + searchQuery: '', settings: props.settings.length === 1 ? props.settings[0] : undefined, submittingToken: false, tokenValidationFailed: false @@ -78,11 +92,27 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat const tokenIsValid = await this.checkPersonalAccessToken(); + let result; + if (tokenIsValid) { + result = await this.fetchProjects(); + } + if (this.mounted) { - this.setState({ - tokenIsValid, - loading: false - }); + if (result) { + const { projects, projectsPaging } = result; + + this.setState({ + tokenIsValid, + loading: false, + projects, + projectsPaging + }); + } else { + this.setState({ + tokenIsValid, + loading: false + }); + } } }; @@ -96,7 +126,61 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat return checkPersonalAccessTokenIsValid(settings.key).catch(() => false); }; - handlePersonalAccessTokenCreate = (token: string) => { + fetchProjects = (pageIndex = 1, query?: string) => { + const { settings } = this.state; + + if (!settings) { + return Promise.resolve(undefined); + } + + return getGitlabProjects({ + almSetting: settings.key, + page: pageIndex, + pageSize: GITLAB_PROJECTS_PAGESIZE, + query + }).catch(() => undefined); + }; + + handleLoadMore = async () => { + this.setState({ loadingMore: true }); + + const { + projectsPaging: { pageIndex }, + searchQuery + } = this.state; + + const result = await this.fetchProjects(pageIndex + 1, searchQuery); + + if (this.mounted) { + this.setState(({ projects = [], projectsPaging }) => ({ + loadingMore: false, + projects: result ? [...projects, ...result.projects] : projects, + projectsPaging: result ? result.projectsPaging : projectsPaging + })); + } + }; + + handleSearch = async (searchQuery: string) => { + this.setState({ searching: true, searchQuery }); + + const result = await this.fetchProjects(1, searchQuery); + + if (this.mounted) { + this.setState(({ projects, projectsPaging }) => ({ + searching: false, + projects: result ? result.projects : projects, + projectsPaging: result ? result.projectsPaging : projectsPaging + })); + } + }; + + cleanUrl = () => { + const { location, router } = this.props; + delete location.query.resetPat; + router.replace(location); + }; + + handlePersonalAccessTokenCreate = async (token: string) => { const { settings } = this.state; if (!settings || token.length < 1) { @@ -104,37 +188,59 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat } this.setState({ submittingToken: true, tokenValidationFailed: false }); - setAlmPersonalAccessToken(settings.key, token) - .then(this.checkPersonalAccessToken) - .then(patIsValid => { - if (this.mounted) { - this.setState({ - submittingToken: false, - tokenIsValid: patIsValid, - tokenValidationFailed: !patIsValid - }); - if (patIsValid) { - this.fetchInitialData(); - } - } - }) - .catch(() => { - if (this.mounted) { - this.setState({ submittingToken: false }); + + try { + await setAlmPersonalAccessToken(settings.key, token); + + const patIsValid = await this.checkPersonalAccessToken(); + + if (this.mounted) { + this.setState({ + submittingToken: false, + tokenIsValid: patIsValid, + tokenValidationFailed: !patIsValid + }); + + if (patIsValid) { + this.cleanUrl(); + await this.fetchInitialData(); } - }); + } + } catch (e) { + if (this.mounted) { + this.setState({ submittingToken: false }); + } + } }; render() { const { canAdmin, loadingBindings, location } = this.props; - const { loading, tokenIsValid, settings, submittingToken, tokenValidationFailed } = this.state; + const { + loading, + loadingMore, + projects, + projectsPaging, + tokenIsValid, + searching, + searchQuery, + settings, + submittingToken, + tokenValidationFailed + } = this.state; return ( <GitlabProjectCreateRenderer settings={settings} canAdmin={canAdmin} loading={loading || loadingBindings} + loadingMore={loadingMore} + onLoadMore={this.handleLoadMore} onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate} + onSearch={this.handleSearch} + projects={projects} + projectsPaging={projectsPaging} + searching={searching} + searchQuery={searchQuery} showPersonalAccessTokenForm={!tokenIsValid || Boolean(location.query.resetPat)} submittingToken={submittingToken} tokenValidationFailed={tokenValidationFailed} diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx index 90f47b85d01..e6a0489d7e2 100644 --- a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx @@ -20,15 +20,24 @@ import * as React from 'react'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { getBaseUrl } from 'sonar-ui-common/helpers/urls'; +import { GitlabProject } from '../../../types/alm-integration'; import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; import CreateProjectPageHeader from './CreateProjectPageHeader'; +import GitlabProjectSelectionForm from './GitlabProjectSelectionForm'; import PersonalAccessTokenForm from './PersonalAccessTokenForm'; import WrongBindingCountAlert from './WrongBindingCountAlert'; export interface GitlabProjectCreateRendererProps { canAdmin?: boolean; loading: boolean; + loadingMore: boolean; + onLoadMore: () => void; onPersonalAccessTokenCreate: (pat: string) => void; + onSearch: (searchQuery: string) => void; + projects?: GitlabProject[]; + projectsPaging: T.Paging; + searching: boolean; + searchQuery: string; settings?: AlmSettingsInstance; showPersonalAccessTokenForm?: boolean; submittingToken?: boolean; @@ -39,6 +48,11 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe const { canAdmin, loading, + loadingMore, + projects, + projectsPaging, + searching, + searchQuery, settings, showPersonalAccessTokenForm, submittingToken, @@ -77,7 +91,15 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe validationFailed={tokenValidationFailed} /> ) : ( - <div>Token is valid!</div> + <GitlabProjectSelectionForm + loadingMore={loadingMore} + onLoadMore={props.onLoadMore} + onSearch={props.onSearch} + projects={projects} + projectsPaging={projectsPaging} + searching={searching} + searchQuery={searchQuery} + /> ))} </> ); diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx new file mode 100644 index 00000000000..01da4fc8851 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx @@ -0,0 +1,149 @@ +/* + * 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 React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router'; +import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; +import SearchBox from 'sonar-ui-common/components/controls/SearchBox'; +import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; +import CheckIcon from 'sonar-ui-common/components/icons/CheckIcon'; +import DetachIcon from 'sonar-ui-common/components/icons/DetachIcon'; +import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; +import { Alert } from 'sonar-ui-common/components/ui/Alert'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { getProjectUrl } from '../../../helpers/urls'; +import { GitlabProject } from '../../../types/alm-integration'; +import { ComponentQualifier } from '../../../types/component'; +import { CreateProjectModes } from './types'; + +export interface GitlabProjectSelectionFormProps { + loadingMore: boolean; + onLoadMore: () => void; + onSearch: (searchQuery: string) => void; + projects?: GitlabProject[]; + projectsPaging: T.Paging; + searching: boolean; + searchQuery: string; +} + +export default function GitlabProjectSelectionForm(props: GitlabProjectSelectionFormProps) { + const { loadingMore, projects = [], projectsPaging, searching, searchQuery } = props; + + if (projects.length === 0 && searchQuery.length === 0 && !searching) { + return ( + <Alert className="spacer-top" variant="warning"> + <FormattedMessage + defaultMessage={translate('onboarding.create_project.gitlab.no_projects')} + id="onboarding.create_project.gitlab.no_projects" + values={{ + link: ( + <Link + to={{ + pathname: '/projects/create', + query: { mode: CreateProjectModes.GitLab, resetPat: 1 } + }}> + {translate('onboarding.create_project.update_your_token')} + </Link> + ) + }} + /> + </Alert> + ); + } + + return ( + <div className="boxed-group big-padded create-project-import-gitlab"> + <SearchBox + className="spacer" + loading={searching} + minLength={3} + onChange={props.onSearch} + placeholder={translate('onboarding.create_project.gitlab.search_prompt')} + /> + + <hr /> + + {projects.length === 0 ? ( + <div className="padded">{translate('no_results')}</div> + ) : ( + <table className="data zebra zebra-hover"> + <tbody> + {projects.map(project => ( + <tr key={project.id}> + <td> + <Tooltip overlay={project.slug}> + <strong className="project-name display-inline-block text-ellipsis"> + {project.name} + </strong> + </Tooltip> + <br /> + <Tooltip overlay={project.pathSlug}> + <span className="text-muted project-path display-inline-block text-ellipsis"> + {project.pathName} + </span> + </Tooltip> + </td> + <td> + <a + className="display-inline-flex-center big-spacer-right" + href={project.url} + rel="noopener noreferrer" + target="_blank"> + <DetachIcon className="little-spacer-right" /> + {translate('onboarding.create_project.gitlab.link')} + </a> + </td> + {project.sqProjectKey ? ( + <> + <td> + <span className="display-flex-center display-flex-justify-end already-set-up"> + <CheckIcon className="little-spacer-right" size={12} /> + {translate('onboarding.create_project.repository_imported')}: + </span> + </td> + <td> + <div className="sq-project-link text-ellipsis"> + <Link to={getProjectUrl(project.sqProjectKey)}> + <QualifierIcon + className="spacer-right" + qualifier={ComponentQualifier.Project} + /> + {project.sqProjectName} + </Link> + </div> + </td> + </> + ) : ( + <td colSpan={2}> </td> + )} + </tr> + ))} + </tbody> + </table> + )} + <ListFooter + count={projects.length} + loadMore={props.onLoadMore} + loading={loadingMore} + total={projectsPaging.total} + /> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx index 4577ba4a0df..072f321528f 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx @@ -186,8 +186,8 @@ it('should handle search', async () => { expect(getGithubRepositories).toBeCalledWith({ almSetting: 'a', organization: 'o1', - p: 1, - ps: 30, + page: 1, + pageSize: 30, query: 'query' }); expect(wrapper.state().repositories).toEqual(repositories); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx index da88c061d27..4dffe5674f5 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx @@ -23,16 +23,19 @@ import * as React from 'react'; import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; import { checkPersonalAccessTokenIsValid, + getGitlabProjects, setAlmPersonalAccessToken } from '../../../../api/alm-integrations'; +import { mockGitlabProject } 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 GitlabProjectCreate from '../GitlabProjectCreate'; jest.mock('../../../../api/alm-integrations', () => ({ checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue(true), - setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null) + setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null), + getGitlabProjects: jest.fn().mockRejectedValue('error') })); beforeEach(jest.clearAllMocks); @@ -75,7 +78,12 @@ it('should correctly handle an invalid PAT', async () => { }); describe('setting a new PAT', () => { - const wrapper = shallowRender(); + const routerReplace = jest.fn(); + const wrapper = shallowRender({ router: mockRouter({ replace: routerReplace }) }); + + beforeEach(() => { + jest.clearAllMocks(); + }); it('should correctly handle it if invalid', async () => { (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(false); @@ -99,9 +107,99 @@ describe('setting a new PAT', () => { expect(checkPersonalAccessTokenIsValid).toBeCalled(); expect(wrapper.state().submittingToken).toBe(false); expect(wrapper.state().tokenValidationFailed).toBe(false); + + expect(routerReplace).toBeCalled(); }); }); +it('should fetch more projects and preserve search', async () => { + (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true); + + const projects = [ + mockGitlabProject({ id: '1' }), + mockGitlabProject({ id: '2' }), + mockGitlabProject({ id: '3' }), + mockGitlabProject({ id: '4' }), + mockGitlabProject({ id: '5' }), + mockGitlabProject({ id: '6' }) + ]; + (getGitlabProjects as jest.Mock) + .mockResolvedValueOnce({ + projects: projects.slice(0, 5), + projectsPaging: { + pageIndex: 1, + pageSize: 4, + total: 6 + } + }) + .mockResolvedValueOnce({ + projects: projects.slice(5), + projectsPaging: { + pageIndex: 2, + pageSize: 4, + total: 6 + } + }); + + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + wrapper.setState({ searchQuery: 'query' }); + + wrapper.instance().handleLoadMore(); + expect(wrapper.state().loadingMore).toBe(true); + + await waitAndUpdate(wrapper); + expect(wrapper.state().loadingMore).toBe(false); + expect(wrapper.state().projects).toEqual(projects); + + expect(getGitlabProjects).toBeCalledWith(expect.objectContaining({ query: 'query' })); +}); + +it('should search for projects', async () => { + (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true); + + const projects = [ + mockGitlabProject({ id: '1' }), + mockGitlabProject({ id: '2' }), + mockGitlabProject({ id: '3' }), + mockGitlabProject({ id: '4' }), + mockGitlabProject({ id: '5' }), + mockGitlabProject({ id: '6' }) + ]; + (getGitlabProjects as jest.Mock) + .mockResolvedValueOnce({ + projects, + projectsPaging: { + pageIndex: 1, + pageSize: 6, + total: 6 + } + }) + .mockResolvedValueOnce({ + projects: projects.slice(3, 5), + projectsPaging: { + pageIndex: 1, + pageSize: 6, + total: 2 + } + }); + const query = 'query'; + + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSearch(query); + expect(wrapper.state().searching).toBe(true); + + await waitAndUpdate(wrapper); + expect(wrapper.state().searching).toBe(false); + expect(wrapper.state().searchQuery).toBe(query); + expect(wrapper.state().projects).toEqual([projects[3], projects[4]]); + + expect(getGitlabProjects).toBeCalledWith(expect.objectContaining({ query })); +}); + function shallowRender(props: Partial<GitlabProjectCreate['props']> = {}) { return shallow<GitlabProjectCreate>( <GitlabProjectCreate @@ -109,6 +207,7 @@ function shallowRender(props: Partial<GitlabProjectCreate['props']> = {}) { loadingBindings={false} location={mockLocation()} onProjectCreate={jest.fn()} + router={mockRouter()} settings={[mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: almSettingKey })]} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx index c824f5ea041..08e6251ddbc 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx @@ -33,6 +33,9 @@ it('should render correctly', () => { 'invalid settings, admin user' ); expect(shallowRender()).toMatchSnapshot('pat form'); + expect(shallowRender({ showPersonalAccessTokenForm: false })).toMatchSnapshot( + 'project selection form' + ); }); function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) { @@ -40,7 +43,14 @@ function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) { <GitlabProjectCreateRenderer canAdmin={false} loading={false} + loadingMore={false} + onLoadMore={jest.fn()} onPersonalAccessTokenCreate={jest.fn()} + onSearch={jest.fn()} + projects={undefined} + projectsPaging={{ pageIndex: 1, pageSize: 30, total: 0 }} + searching={false} + searchQuery="" showPersonalAccessTokenForm={true} submittingToken={false} tokenValidationFailed={false} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx new file mode 100644 index 00000000000..2840d073908 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations'; +import GitlabProjectSelectionForm, { + GitlabProjectSelectionFormProps +} from '../GitlabProjectSelectionForm'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('projects'); + + expect(shallowRender({ projects: undefined, projectsPaging: mockPaging() })).toMatchSnapshot( + 'undefined projects' + ); + expect(shallowRender({ projects: [], projectsPaging: mockPaging() })).toMatchSnapshot( + 'no projects' + ); + expect( + shallowRender({ projects: [], projectsPaging: mockPaging(), searchQuery: 'findme' }) + ).toMatchSnapshot('no projects when searching'); +}); + +function shallowRender(props: Partial<GitlabProjectSelectionFormProps> = {}) { + const projects = [ + mockGitlabProject(), + mockGitlabProject({ + id: '2', + sqProjectKey: 'already-imported', + sqProjectName: 'Already Imported' + }) + ]; + + return shallow<GitlabProjectSelectionFormProps>( + <GitlabProjectSelectionForm + loadingMore={false} + onLoadMore={jest.fn()} + onSearch={jest.fn()} + projects={projects} + projectsPaging={mockPaging(projects.length)} + searching={false} + searchQuery="" + {...props} + /> + ); +} + +function mockPaging(total = 0) { + return { total, pageIndex: 1, pageSize: 30 }; +} 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 3c54a4b2d4d..8dd4ffc6bc8 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 @@ -174,6 +174,19 @@ exports[`should render correctly if the GitLab 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/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap index 8bf4d3701b3..08c98497cc1 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap @@ -4,7 +4,19 @@ exports[`should render correctly 1`] = ` <GitlabProjectCreateRenderer canAdmin={false} loading={true} + loadingMore={false} + onLoadMore={[Function]} onPersonalAccessTokenCreate={[Function]} + onSearch={[Function]} + projectsPaging={ + Object { + "pageIndex": 1, + "pageSize": 30, + "total": 0, + } + } + searchQuery="" + searching={false} settings={ Object { "alm": "gitlab", diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap index d0d2a723737..eb03b51d1d5 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap @@ -101,3 +101,37 @@ exports[`should render correctly: pat form 1`] = ` /> </Fragment> `; + +exports[`should render correctly: project selection form 1`] = ` +<Fragment> + <CreateProjectPageHeader + title={ + <span + className="text-middle" + > + <img + alt="" + className="spacer-right" + height="24" + src="/images/alm/gitlab.svg" + /> + onboarding.create_project.gitlab.title + </span> + } + /> + <GitlabProjectSelectionForm + loadingMore={false} + onLoadMore={[MockFunction]} + onSearch={[MockFunction]} + projectsPaging={ + Object { + "pageIndex": 1, + "pageSize": 30, + "total": 0, + } + } + searchQuery="" + searching={false} + /> +</Fragment> +`; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap new file mode 100644 index 00000000000..eb9bf997e36 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: no projects 1`] = ` +<Alert + className="spacer-top" + variant="warning" +> + <FormattedMessage + defaultMessage="onboarding.create_project.gitlab.no_projects" + id="onboarding.create_project.gitlab.no_projects" + values={ + Object { + "link": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/projects/create", + "query": Object { + "mode": "gitlab", + "resetPat": 1, + }, + } + } + > + onboarding.create_project.update_your_token + </Link>, + } + } + /> +</Alert> +`; + +exports[`should render correctly: no projects when searching 1`] = ` +<div + className="boxed-group big-padded create-project-import-gitlab" +> + <SearchBox + className="spacer" + loading={false} + minLength={3} + onChange={[MockFunction]} + placeholder="onboarding.create_project.gitlab.search_prompt" + /> + <hr /> + <div + className="padded" + > + no_results + </div> + <ListFooter + count={0} + loadMore={[MockFunction]} + loading={false} + total={0} + /> +</div> +`; + +exports[`should render correctly: projects 1`] = ` +<div + className="boxed-group big-padded create-project-import-gitlab" +> + <SearchBox + className="spacer" + loading={false} + minLength={3} + onChange={[MockFunction]} + placeholder="onboarding.create_project.gitlab.search_prompt" + /> + <hr /> + <table + className="data zebra zebra-hover" + > + <tbody> + <tr + key="id1234" + > + <td> + <Tooltip + overlay="awesome-project-exclamation" + > + <strong + className="project-name display-inline-block text-ellipsis" + > + Awesome Project ! + </strong> + </Tooltip> + <br /> + <Tooltip + overlay="company/best-projects" + > + <span + className="text-muted project-path display-inline-block text-ellipsis" + > + Company / Best Projects + </span> + </Tooltip> + </td> + <td> + <a + className="display-inline-flex-center big-spacer-right" + href="https://gitlab.company.com/best-projects/awesome-project-exclamation" + rel="noopener noreferrer" + target="_blank" + > + <DetachIcon + className="little-spacer-right" + /> + onboarding.create_project.gitlab.link + </a> + </td> + <td + colSpan={2} + > + + </td> + </tr> + <tr + key="2" + > + <td> + <Tooltip + overlay="awesome-project-exclamation" + > + <strong + className="project-name display-inline-block text-ellipsis" + > + Awesome Project ! + </strong> + </Tooltip> + <br /> + <Tooltip + overlay="company/best-projects" + > + <span + className="text-muted project-path display-inline-block text-ellipsis" + > + Company / Best Projects + </span> + </Tooltip> + </td> + <td> + <a + className="display-inline-flex-center big-spacer-right" + href="https://gitlab.company.com/best-projects/awesome-project-exclamation" + rel="noopener noreferrer" + target="_blank" + > + <DetachIcon + className="little-spacer-right" + /> + onboarding.create_project.gitlab.link + </a> + </td> + <td> + <span + className="display-flex-center display-flex-justify-end already-set-up" + > + <CheckIcon + className="little-spacer-right" + size={12} + /> + onboarding.create_project.repository_imported + : + </span> + </td> + <td> + <div + className="sq-project-link text-ellipsis" + > + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "already-imported", + }, + } + } + > + <QualifierIcon + className="spacer-right" + qualifier="TRK" + /> + Already Imported + </Link> + </div> + </td> + </tr> + </tbody> + </table> + <ListFooter + count={2} + loadMore={[MockFunction]} + loading={false} + total={2} + /> +</div> +`; + +exports[`should render correctly: undefined projects 1`] = ` +<Alert + className="spacer-top" + variant="warning" +> + <FormattedMessage + defaultMessage="onboarding.create_project.gitlab.no_projects" + id="onboarding.create_project.gitlab.no_projects" + values={ + Object { + "link": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/projects/create", + "query": Object { + "mode": "gitlab", + "resetPat": 1, + }, + } + } + > + onboarding.create_project.update_your_token + </Link>, + } + } + /> +</Alert> +`; diff --git a/server/sonar-web/src/main/js/apps/create/project/style.css b/server/sonar-web/src/main/js/apps/create/project/style.css index 7930a3b216e..b02012dfe23 100644 --- a/server/sonar-web/src/main/js/apps/create/project/style.css +++ b/server/sonar-web/src/main/js/apps/create/project/style.css @@ -60,3 +60,20 @@ .create-project-github-repository .notice svg { color: var(--green); } + +.create-project-import-gitlab table > tbody > tr > td { + vertical-align: middle; +} + +.create-project-import-gitlab .project-name, +.create-project-import-gitlab .project-path { + max-width: 400px; +} + +.create-project-import-gitlab .sq-project-link { + max-width: 300px; +} + +.create-project-import-gitlab .already-set-up svg { + color: var(--green); +} 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 b4a1275fb58..1fdd37225e6 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 @@ -20,7 +20,8 @@ import { BitbucketProject, BitbucketRepository, - GithubRepository + GithubRepository, + GitlabProject } from '../../types/alm-integration'; export function mockBitbucketProject(overrides: Partial<BitbucketProject> = {}): BitbucketProject { @@ -54,3 +55,16 @@ export function mockGitHubRepository(overrides: Partial<GithubRepository> = {}): ...overrides }; } + +export function mockGitlabProject(overrides: Partial<GitlabProject> = {}): GitlabProject { + return { + id: 'id1234', + name: 'Awesome Project !', + slug: 'awesome-project-exclamation', + pathName: 'Company / Best Projects', + pathSlug: 'company/best-projects', + sqProjectKey: '', + url: 'https://gitlab.company.com/best-projects/awesome-project-exclamation', + ...overrides + }; +} 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 4a2a5864e8c..cf70f73017a 100644 --- a/server/sonar-web/src/main/js/types/alm-integration.ts +++ b/server/sonar-web/src/main/js/types/alm-integration.ts @@ -48,3 +48,14 @@ export interface GithubRepository { url: string; sqProjectKey: string; } + +export interface GitlabProject { + id: string; + name: string; + pathName: string; + pathSlug: string; + sqProjectKey?: string; + sqProjectName?: string; + slug: string; + url: string; +} |