From 1fba92e6a849ab29a6d0889dfa56bbc60b5f275c Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Mon, 3 Feb 2020 15:49:36 +0100 Subject: [PATCH] SONAR-13001 Add new option for BBS project import --- .../sonar-web/src/main/js/api/almSettings.ts | 2 +- .../src/main/js/app/styles/init/misc.css | 10 + .../project/BitbucketImportRepositoryForm.tsx | 131 ++++++ .../BitbucketPersonalAccessTokenForm.tsx | 142 ++++++ .../create/project/BitbucketProjectCreate.tsx | 243 ++++++++++ .../BitbucketProjectCreateRenderer.tsx | 139 ++++++ .../project/CreateProjectModeSelection.tsx | 102 ++++ .../project/CreateProjectPageHeader.tsx | 48 ++ .../project/CreateProjectPageSonarQube.tsx | 119 ++++- .../create/project/ManualProjectCreate.tsx | 182 ++++---- .../BitbucketImportRepositoryForm-test.tsx | 84 ++++ .../BitbucketPersonalAccessTokenForm-test.tsx | 64 +++ .../__tests__/BitbucketProjectCreate-test.tsx | 139 ++++++ .../BitbucketProjectCreateRenderer-test.tsx | 64 +++ .../CreateProjectModeSelection-test.tsx | 56 +++ .../CreateProjectPageHeader-test.tsx | 33 ++ .../CreateProjectPageSonarQube-test.tsx | 85 ++++ ...itbucketImportRepositoryForm-test.tsx.snap | 440 ++++++++++++++++++ ...ucketPersonalAccessTokenForm-test.tsx.snap | 225 +++++++++ .../BitbucketProjectCreate-test.tsx.snap | 19 + ...tbucketProjectCreateRenderer-test.tsx.snap | 383 +++++++++++++++ .../CreateProjectModeSelection-test.tsx.snap | 267 +++++++++++ .../CreateProjectPage-test.tsx.snap | 2 +- .../CreateProjectPageHeader-test.tsx.snap | 48 ++ .../CreateProjectPageSonarQube-test.tsx.snap | 114 +++++ .../ManualProjectCreate-test.tsx.snap | 149 +++--- .../src/main/js/apps/create/project/style.css | 37 ++ .../src/main/js/apps/create/project/types.ts | 23 + .../src/main/js/helpers/mocks/alm-settings.ts | 12 + .../resources/org/sonar/l10n/core.properties | 35 +- 30 files changed, 3217 insertions(+), 180 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/CreateProjectPageHeader.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketImportRepositoryForm-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketPersonalAccessTokenForm-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageHeader-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageSonarQube-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageHeader-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageSonarQube-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/project/types.ts diff --git a/server/sonar-web/src/main/js/api/almSettings.ts b/server/sonar-web/src/main/js/api/almSettings.ts index 897afcef16e..6ac6b1c0963 100644 --- a/server/sonar-web/src/main/js/api/almSettings.ts +++ b/server/sonar-web/src/main/js/api/almSettings.ts @@ -37,7 +37,7 @@ export function getAlmDefinitions(): Promise { return getJSON('/api/alm_settings/list_definitions').catch(throwGlobalError); } -export function getAlmSettings(project: string): Promise { +export function getAlmSettings(project?: string): Promise { return getJSON('/api/alm_settings/list', { project }) .then(({ almSettings }) => almSettings) .catch(throwGlobalError); diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index 9fa692cbb93..c4e543c8f39 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -380,6 +380,11 @@ th.huge-spacer-right { align-items: flex-start; } +.display-flex-wrap { + display: flex !important; + flex-wrap: wrap; +} + .display-inline-flex-baseline { display: inline-flex !important; align-items: baseline; @@ -390,6 +395,11 @@ th.huge-spacer-right { align-items: center; } +.display-inline-flex-start { + display: inline-flex !important; + align-items: start; +} + .position-absolute { position: absolute !important; } diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx new file mode 100644 index 00000000000..55ba5b2ffb9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketImportRepositoryForm.tsx @@ -0,0 +1,131 @@ +/* + * 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 { uniq, without } from 'lodash'; +import * as React from 'react'; +import { Link } from 'react-router'; +import BoxedGroupAccordion from 'sonar-ui-common/components/controls/BoxedGroupAccordion'; +import { ButtonLink } from 'sonar-ui-common/components/controls/buttons'; +import Radio from 'sonar-ui-common/components/controls/Radio'; +import CheckIcon from 'sonar-ui-common/components/icons/CheckIcon'; +import { Alert } from 'sonar-ui-common/components/ui/Alert'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { colors } from '../../../app/theme'; +import { getProjectUrl } from '../../../helpers/urls'; +import { BitbucketProject, BitbucketRepository } from '../../../types/alm-integration'; + +export interface BitbucketImportRepositoryFormProps { + importing?: boolean; + onSelectRepository: (repo: BitbucketRepository) => void; + projects?: BitbucketProject[]; + projectRepositories?: T.Dict; + selectedRepository?: BitbucketRepository; +} + +export default function BitbucketImportRepositoryForm(props: BitbucketImportRepositoryFormProps) { + const { importing, projects = [], projectRepositories = {}, selectedRepository } = props; + const [openProjectKeys, setOpenProjectKeys] = React.useState( + projects.length > 0 ? [projects[0].key] : [] + ); + + if (projects.length === 0) { + return ( + {translate('onboarding.create_project.no_bbs_projects')} + ); + } + + const allAreExpanded = projects.length === openProjectKeys.length; + + return ( +
+
+ setOpenProjectKeys(allAreExpanded ? [] : projects.map(p => p.key))}> + {allAreExpanded ? translate('collapse_all') : translate('expand_all')} + +
+ + {projects.map(project => { + const isOpen = openProjectKeys.includes(project.key); + const repositories = projectRepositories[project.key] || []; + + return ( + + setOpenProjectKeys( + isOpen + ? without(openProjectKeys, project.key) + : uniq([...openProjectKeys, project.key]) + ) + } + open={isOpen} + title={

{project.name}

}> + {isOpen && ( +
+ {repositories.length === 0 && ( + + {translate('onboarding.create_project.no_bbs_repos')} + + )} + + {repositories.map(repo => + repo.sqProjectKey ? ( + + + +
+ + {repo.name} + +
+ {translate('onboarding.create_project.repository_imported')} +
+
+ ) : ( + props.onSelectRepository(repo)} + value={String(repo.id)}> + {repo.name} + + ) + )} +
+ )} +
+ ); + })} +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx new file mode 100644 index 00000000000..8d5a07e3161 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx @@ -0,0 +1,142 @@ +/* + * 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; +import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput'; +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 { getBaseUrl } from 'sonar-ui-common/helpers/urls'; +import { AlmSettingsInstance } from '../../../types/alm-settings'; + +export interface BitbucketPersonalAccessTokenFormProps { + bitbucketSetting: AlmSettingsInstance; + onPersonalAccessTokenCreate: (token: string) => void; + submitting?: boolean; +} + +export default function BitbucketPersonalAccessTokenForm( + props: BitbucketPersonalAccessTokenFormProps +) { + const { + bitbucketSetting: { url }, + submitting = false + } = props; + const [personalAccessToken, setPersonalAccessToken] = React.useState(''); + const isValid = personalAccessToken.length > 0; + + return ( +
+
) => { + e.preventDefault(); + props.onPersonalAccessTokenCreate(personalAccessToken); + }}> +

{translate('onboarding.create_project.grant_access_to_bbs.title')}

+

+ {translate('onboarding.create_project.grant_access_to_bbs.help')} +

+ + + ) => + setPersonalAccessToken(e.currentTarget.value) + } + type="text" + value={personalAccessToken} + /> + + + {translate('save')} + + + + +

{translate('onboarding.create_project.pat_help.title')}

+ +

+ {translate('onboarding.create_project.pat_help.bbs_help_1')} +

+ + {url && ( + + )} + +

+ {translate('onboarding.create_project.pat_help.bbs_help_2')} +

+ +
    +
  • + {translate('onboarding.create_project.pat_help.read_permission')} + ) + }} + /> +
  • +
  • + {translate('onboarding.create_project.pat_help.read_permission')} + ) + }} + /> +
  • +
+
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx new file mode 100644 index 00000000000..931302f357d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx @@ -0,0 +1,243 @@ +/* + * 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 { connect } from 'react-redux'; +import { WithRouterProps } from 'react-router'; +import { + checkPersonalAccessTokenIsValid, + getBitbucketServerProjects, + getBitbucketServerRepositories, + importBitbucketServerProject, + setAlmPersonalAccessToken +} from '../../../api/alm-integrations'; +import { getAppState, Store } from '../../../store/rootReducer'; +import { BitbucketProject, BitbucketRepository } from '../../../types/alm-integration'; +import { AlmSettingsInstance } from '../../../types/alm-settings'; +import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer'; + +interface Props extends Pick { + bitbucketSettings: AlmSettingsInstance[]; + canAdmin?: boolean; + loadingBindings: boolean; + onProjectCreate: (projectKeys: string[]) => void; +} + +interface State { + bitbucketSetting?: AlmSettingsInstance; + importing: boolean; + loading: boolean; + patIsValid?: boolean; + projects?: BitbucketProject[]; + projectRepositories?: T.Dict; + selectedRepository?: BitbucketRepository; + submittingToken?: boolean; +} + +export class BitbucketProjectCreate extends React.PureComponent { + mounted = false; + + constructor(props: Props) { + super(props); + this.state = { + // For now, we only handle a single instance. So we always use the first + // one from the list. + bitbucketSetting: props.bitbucketSettings[0], + importing: false, + loading: false + }; + } + + componentDidMount() { + this.mounted = true; + this.fetchInitialData(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.bitbucketSettings.length === 0 && this.props.bitbucketSettings.length > 0) { + this.setState({ bitbucketSetting: this.props.bitbucketSettings[0] }); + this.fetchInitialData(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchInitialData = async () => { + this.setState({ loading: true }); + + const patIsValid = await this.checkPersonalAccessToken().catch(() => false); + + let projects; + if (patIsValid) { + projects = await this.fetchBitbucketProjects().catch(() => undefined); + } + + let projectRepositories; + if (projects && projects.length > 0) { + projectRepositories = await this.fetchBitbucketRepositories(projects).catch(() => undefined); + } + + if (this.mounted) { + this.setState({ + patIsValid, + projects, + projectRepositories, + loading: false + }); + } + }; + + checkPersonalAccessToken = () => { + const { bitbucketSetting } = this.state; + + if (!bitbucketSetting) { + return Promise.resolve(false); + } + + return checkPersonalAccessTokenIsValid(bitbucketSetting.key); + }; + + fetchBitbucketProjects = (): Promise => { + const { bitbucketSetting } = this.state; + + if (!bitbucketSetting) { + return Promise.resolve(undefined); + } + + return getBitbucketServerProjects(bitbucketSetting.key).then(({ projects }) => projects); + }; + + fetchBitbucketRepositories = ( + projects: BitbucketProject[] + ): Promise | undefined> => { + const { bitbucketSetting } = this.state; + + if (!bitbucketSetting) { + return Promise.resolve(undefined); + } + + return Promise.all( + projects.map(p => { + return getBitbucketServerRepositories(bitbucketSetting.key, p.name).then( + ({ repositories }) => ({ + repositories, + projectKey: p.key + }) + ); + }) + ).then(results => { + return results.reduce((acc: T.Dict, { projectKey, repositories }) => { + return { ...acc, [projectKey]: repositories }; + }, {}); + }); + }; + + handlePersonalAccessTokenCreate = (token: string) => { + const { bitbucketSetting } = this.state; + + if (!bitbucketSetting || token.length < 1) { + return; + } + + this.setState({ submittingToken: true }); + setAlmPersonalAccessToken(bitbucketSetting.key, token) + .then(() => { + if (this.mounted) { + this.setState({ submittingToken: false }); + this.fetchInitialData(); + } + }) + .catch(() => { + if (this.mounted) { + this.setState({ submittingToken: false }); + } + }); + }; + + handleImportRepository = () => { + const { bitbucketSetting, selectedRepository } = this.state; + + if (!bitbucketSetting || !selectedRepository) { + return; + } + + this.setState({ importing: true }); + importBitbucketServerProject( + bitbucketSetting.key, + selectedRepository.projectKey, + selectedRepository.slug + ) + .then(({ project: { key } }) => { + if (this.mounted) { + this.setState({ importing: false }); + this.props.onProjectCreate([key]); + } + }) + .catch(() => { + if (this.mounted) { + this.setState({ importing: false }); + } + }); + }; + + handleSelectRepository = (selectedRepository: BitbucketRepository) => { + this.setState({ selectedRepository }); + }; + + render() { + const { canAdmin, loadingBindings } = this.props; + const { + bitbucketSetting, + importing, + loading, + patIsValid, + projectRepositories, + projects, + selectedRepository, + submittingToken + } = this.state; + + return ( + + ); + } +} + +const mapStateToProps = (state: Store) => { + const { canAdmin } = getAppState(state); + return { canAdmin }; +}; + +export default connect(mapStateToProps)(BitbucketProjectCreate); diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx new file mode 100644 index 00000000000..1a6a596462a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx @@ -0,0 +1,139 @@ +/* + * 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 { Button } from 'sonar-ui-common/components/controls/buttons'; +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 { getBaseUrl } from 'sonar-ui-common/helpers/urls'; +import { BitbucketProject, BitbucketRepository } from '../../../types/alm-integration'; +import { AlmSettingsInstance } from '../../../types/alm-settings'; +import { PULL_REQUEST_DECORATION_BINDING_CATEGORY } from '../../settings/components/AdditionalCategoryKeys'; +import BitbucketImportRepositoryForm from './BitbucketImportRepositoryForm'; +import BitbucketPersonalAccessTokenForm from './BitbucketPersonalAccessTokenForm'; +import CreateProjectPageHeader from './CreateProjectPageHeader'; + +export interface BitbucketProjectCreateRendererProps { + bitbucketSetting?: AlmSettingsInstance; + canAdmin?: boolean; + importing: boolean; + loading: boolean; + onImportRepository: () => void; + onSelectRepository: (repo: BitbucketRepository) => void; + onPersonalAccessTokenCreate: (token: string) => void; + onProjectCreate: (projectKeys: string[]) => void; + projects?: BitbucketProject[]; + projectRepositories?: T.Dict; + selectedRepository?: BitbucketRepository; + showPersonalAccessTokenForm?: boolean; + submittingToken?: boolean; +} + +export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCreateRendererProps) { + const { + bitbucketSetting, + canAdmin, + importing, + loading, + projects, + projectRepositories, + selectedRepository, + showPersonalAccessTokenForm, + submittingToken + } = props; + + return ( + <> + + + + + ) + } + showBreadcrumb={true} + title={ + + + {translate('onboarding.create_project.from_bbs')} + + } + /> + + {loading && } + + {!loading && !bitbucketSetting && ( + + {canAdmin ? ( + + {translate('settings.page')} + + ) + }} + /> + ) : ( + translate('onboarding.create_project.no_bbs_binding') + )} + + )} + + {!loading && + bitbucketSetting && + (showPersonalAccessTokenForm ? ( + + ) : ( + + ))} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx new file mode 100644 index 00000000000..eb8d1e5a7a5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx @@ -0,0 +1,102 @@ +/* + * 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 HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; +import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { getBaseUrl } from 'sonar-ui-common/helpers/urls'; +import { CreateProjectModes } from './types'; + +export interface CreateProjectModeSelectionProps { + bbsBindingCount: number; + loadingBindings: boolean; + onSelectMode: (mode: CreateProjectModes) => void; +} + +export default function CreateProjectModeSelection(props: CreateProjectModeSelectionProps) { + const { bbsBindingCount, loadingBindings } = props; + + return ( + <> +
+

+ {translate('my_account.create_new.TRK')} +

+

{translate('onboarding.create_project.select_method')}

+
+ +
+ + + +
+ + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPageHeader.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPageHeader.tsx new file mode 100644 index 00000000000..1cbd7c56918 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPageHeader.tsx @@ -0,0 +1,48 @@ +/* + * 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 { Link } from 'react-router'; +import { translate } from 'sonar-ui-common/helpers/l10n'; + +export interface CreateProjectPageHeaderProps { + additionalActions?: React.ReactNode; + showBreadcrumb?: boolean; + title: React.ReactNode; +} + +export default function CreateProjectPageHeader(props: CreateProjectPageHeaderProps) { + const { additionalActions, showBreadcrumb, title } = props; + + return ( +
+

+ {showBreadcrumb && ( + <> + {translate('my_account.create_new.TRK')} + + + )} + {title} +

+ + {additionalActions} +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPageSonarQube.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPageSonarQube.tsx index aa88ce27183..d0a8ced9353 100644 --- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPageSonarQube.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPageSonarQube.tsx @@ -22,50 +22,135 @@ import { Helmet } from 'react-helmet-async'; import { WithRouterProps } from 'react-router'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { addWhitePageClass, removeWhitePageClass } from 'sonar-ui-common/helpers/pages'; +import { getAlmSettings } from '../../../api/almSettings'; import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn'; +import { withAppState } from '../../../components/hoc/withAppState'; import { getProjectUrl } from '../../../helpers/urls'; +import { AlmSettingsInstance, ALM_KEYS } from '../../../types/alm-settings'; +import BitbucketProjectCreate from './BitbucketProjectCreate'; +import CreateProjectModeSelection from './CreateProjectModeSelection'; import ManualProjectCreate from './ManualProjectCreate'; import './style.css'; +import { CreateProjectModes } from './types'; -interface Props { +interface Props extends Pick { + appState: Pick; currentUser: T.LoggedInUser; } -export class CreateProjectPageSonarQube extends React.PureComponent { +interface State { + bitbucketSettings: AlmSettingsInstance[]; + loading: boolean; +} + +export class CreateProjectPageSonarQube extends React.PureComponent { + mounted = false; + state: State = { bitbucketSettings: [], loading: false }; + componentDidMount() { - addWhitePageClass(); + const { + appState: { branchesEnabled }, + location + } = this.props; + this.mounted = true; + if (branchesEnabled) { + this.fetchAlmBindings(); + } + + if (location.query?.mode || !branchesEnabled) { + addWhitePageClass(); + } + } + + componentDidUpdate(prevProps: Props) { + if (this.props.location.query?.mode && !prevProps.location.query?.mode) { + addWhitePageClass(); + } else if (!this.props.location.query?.mode && prevProps.location.query?.mode) { + removeWhitePageClass(); + } } componentWillUnmount() { + this.mounted = false; removeWhitePageClass(); } + fetchAlmBindings = () => { + this.setState({ loading: true }); + getAlmSettings() + .then(almSettings => { + if (this.mounted) { + this.setState({ + bitbucketSettings: almSettings.filter(s => s.alm === ALM_KEYS.BITBUCKET), + loading: false + }); + } + }) + .catch(() => { + if (this.mounted) { + this.setState({ loading: false }); + } + }); + }; + handleProjectCreate = (projectKeys: string[]) => { if (projectKeys.length === 1) { this.props.router.push(getProjectUrl(projectKeys[0])); } }; + handleModeSelect = (mode: CreateProjectModes) => { + const { router, location } = this.props; + router.push({ + pathname: location.pathname, + query: { mode } + }); + }; + render() { - const { currentUser } = this.props; - const header = translate('my_account.create_new.TRK'); + const { + appState: { branchesEnabled }, + currentUser, + location + } = this.props; + const { bitbucketSettings, loading } = this.state; + + const mode: CreateProjectModes | undefined = location.query?.mode; + const showManualForm = !branchesEnabled || mode === CreateProjectModes.Manual; + const showBBSForm = branchesEnabled && mode === CreateProjectModes.BitbucketServer; + return ( <> - -
-
-

- {header} -

-
- + +
+ {!showBBSForm && !showManualForm && ( + + )} + + {showManualForm && ( + + )} + + {showBBSForm && ( + + )}
); } } -export default whenLoggedIn(CreateProjectPageSonarQube); +export default whenLoggedIn(withAppState(CreateProjectPageSonarQube)); diff --git a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx index 8138857a326..3d983abd485 100644 --- a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx @@ -28,10 +28,12 @@ import { createProject, doesComponentExists } from '../../../api/components'; import VisibilitySelector from '../../../components/common/VisibilitySelector'; import { isSonarCloud } from '../../../helpers/system'; import UpgradeOrganizationBox from '../components/UpgradeOrganizationBox'; +import CreateProjectPageHeader from './CreateProjectPageHeader'; import './ManualProjectCreate.css'; import OrganizationInput from './OrganizationInput'; interface Props { + branchesEnabled?: boolean; currentUser: T.LoggedInUser; fetchMyOrganizations?: () => Promise; onProjectCreate: (projectKeys: string[]) => void; @@ -248,6 +250,7 @@ export default class ManualProjectCreate extends React.PureComponent -
-
- {isSonarCloud() && this.props.userOrganizations && ( - - )} - - - + + +
+
+ + {isSonarCloud() && this.props.userOrganizations && ( + + )} + + - - - - + + + + - - - {isSonarCloud() && selectedOrganization && ( -
- + -
- )} - - - {translate('set_up')} - - - -
+ + + {isSonarCloud() && selectedOrganization && ( +
+ +
+ )} - {isSonarCloud() && selectedOrganization && ( -
- + + {translate('set_up')} + + +
- )} -
+ + {isSonarCloud() && selectedOrganization && ( +
+ +
+ )} +
+ ); } } diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketImportRepositoryForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketImportRepositoryForm-test.tsx new file mode 100644 index 00000000000..f6c37b52157 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketImportRepositoryForm-test.tsx @@ -0,0 +1,84 @@ +/* + * 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. + */ +/* eslint-disable sonarjs/no-duplicate-string */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import BoxedGroupAccordion from 'sonar-ui-common/components/controls/BoxedGroupAccordion'; +import Radio from 'sonar-ui-common/components/controls/Radio'; +import { click } from 'sonar-ui-common/helpers/testUtils'; +import { + mockBitbucketProject, + mockBitbucketRepository +} from '../../../../helpers/mocks/alm-integrations'; +import BitbucketImportRepositoryForm, { + BitbucketImportRepositoryFormProps +} from '../BitbucketImportRepositoryForm'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ importing: true })).toMatchSnapshot('importing'); + expect(shallowRender({ projects: [] })).toMatchSnapshot('no projects'); + expect(shallowRender({ projectRepositories: {} })).toMatchSnapshot('no repos'); + expect(shallowRender({ selectedRepository: mockBitbucketRepository() })).toMatchSnapshot( + 'selected repo' + ); +}); + +it('should correctly handle opening/closing accordions', () => { + const wrapper = shallowRender(); + click(wrapper.find(BoxedGroupAccordion).at(1)); + expect(wrapper).toMatchSnapshot('2nd opened'); +}); + +it('should correctly handle selecting repos', () => { + const onSelectRepository = jest.fn(); + const repo = mockBitbucketRepository(); + const wrapper = shallowRender({ + onSelectRepository, + projectRepositories: { + project: [repo] + } + }); + + wrapper + .find(Radio) + .at(0) + .prop('onCheck')(); + expect(onSelectRepository).toBeCalledWith(repo); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketPersonalAccessTokenForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketPersonalAccessTokenForm-test.tsx new file mode 100644 index 00000000000..316871fe7c5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketPersonalAccessTokenForm-test.tsx @@ -0,0 +1,64 @@ +/* + * 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; +import { change, submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; +import { ALM_KEYS } from '../../../../types/alm-settings'; +import BitbucketPersonalAccessTokenForm, { + BitbucketPersonalAccessTokenFormProps +} from '../BitbucketPersonalAccessTokenForm'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); +}); + +it('should correctly handle form interactions', async () => { + const onPersonalAccessTokenCreate = jest.fn(); + const wrapper = shallowRender({ onPersonalAccessTokenCreate }); + + // Submit button disabled by default. + expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true); + + // Submit button enabled if there's a value. + change(wrapper.find('input'), 'token'); + expect(wrapper.find(SubmitButton).prop('disabled')).toBe(false); + + // Expect correct calls to be made when submitting. + submit(wrapper.find('form')); + await waitAndUpdate(wrapper); + expect(onPersonalAccessTokenCreate).toBeCalled(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx new file mode 100644 index 00000000000..91fb4650308 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx @@ -0,0 +1,139 @@ +/* + * 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { + checkPersonalAccessTokenIsValid, + getBitbucketServerProjects, + getBitbucketServerRepositories, + importBitbucketServerProject, + setAlmPersonalAccessToken +} from '../../../../api/alm-integrations'; +import { mockBitbucketRepository } from '../../../../helpers/mocks/alm-integrations'; +import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; +import { mockLocation } from '../../../../helpers/testMocks'; +import { ALM_KEYS } from '../../../../types/alm-settings'; +import { BitbucketProjectCreate } from '../BitbucketProjectCreate'; + +jest.mock('../../../../api/alm-integrations', () => { + const { mockBitbucketProject, mockBitbucketRepository } = jest.requireActual( + '../../../../helpers/mocks/alm-integrations' + ); + return { + checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue(true), + getBitbucketServerProjects: jest.fn().mockResolvedValue({ + projects: [ + mockBitbucketProject({ key: 'project1', name: 'Project 1' }), + mockBitbucketProject({ id: 2, key: 'project2' }) + ] + }), + getBitbucketServerRepositories: jest.fn().mockResolvedValue({ + repositories: [ + mockBitbucketRepository(), + mockBitbucketRepository({ id: 2, slug: 'project__repo2' }) + ] + }), + importBitbucketServerProject: jest.fn().mockResolvedValue({ project: { key: 'baz' } }), + setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null) + }; +}); + +beforeEach(jest.clearAllMocks); + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should correctly fetch binding info on mount', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(checkPersonalAccessTokenIsValid).toBeCalledWith('foo'); +}); + +it('should correctly handle a valid PAT', async () => { + (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true); + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(checkPersonalAccessTokenIsValid).toBeCalled(); + expect(wrapper.state().patIsValid).toBe(true); +}); + +it('should correctly handle an invalid PAT', async () => { + (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(false); + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(checkPersonalAccessTokenIsValid).toBeCalled(); + expect(wrapper.state().patIsValid).toBe(false); +}); + +it('should correctly handle setting a new PAT', () => { + const wrapper = shallowRender(); + wrapper.instance().handlePersonalAccessTokenCreate('token'); + expect(setAlmPersonalAccessToken).toBeCalledWith('foo', 'token'); +}); + +it('should correctly fetch projects and repos', async () => { + const wrapper = shallowRender(); + + // Opens first project on mount. + await waitAndUpdate(wrapper); + expect(getBitbucketServerProjects).toBeCalledWith('foo'); + expect(wrapper.state().projects).toHaveLength(2); + + // Check repos got loaded. + await waitAndUpdate(wrapper); + expect(getBitbucketServerRepositories).toBeCalledWith('foo', 'Project 1'); + expect(wrapper.state().projectRepositories).toEqual( + expect.objectContaining({ + project1: expect.arrayContaining([ + expect.objectContaining({ id: 1 }), + expect.objectContaining({ id: 2 }) + ]) + }) + ); + expect(wrapper.state().projectRepositories).toBeDefined(); +}); + +it('should correctly import a repo', async () => { + const onProjectCreate = jest.fn(); + const repo = mockBitbucketRepository(); + const wrapper = shallowRender({ onProjectCreate }); + const instance = wrapper.instance(); + + instance.handleSelectRepository(repo); + instance.handleImportRepository(); + expect(importBitbucketServerProject).toBeCalledWith('foo', repo.projectKey, repo.slug); + await waitAndUpdate(wrapper); + expect(onProjectCreate).toBeCalledWith(['baz']); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx new file mode 100644 index 00000000000..66e237bc92b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx @@ -0,0 +1,64 @@ +/* + * 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 { + mockBitbucketProject, + mockBitbucketRepository +} from '../../../../helpers/mocks/alm-integrations'; +import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; +import { ALM_KEYS } from '../../../../types/alm-settings'; +import BitbucketProjectCreateRenderer, { + BitbucketProjectCreateRendererProps +} from '../BitbucketProjectCreateRenderer'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ showPersonalAccessTokenForm: true })).toMatchSnapshot('pat form'); + expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); + expect(shallowRender({ importing: true })).toMatchSnapshot('importing'); + expect(shallowRender({ selectedRepository: mockBitbucketRepository() })).toMatchSnapshot( + 'selected repo' + ); + expect(shallowRender({ bitbucketSetting: undefined })).toMatchSnapshot( + 'invalid config, regular user' + ); + expect(shallowRender({ bitbucketSetting: undefined, canAdmin: true })).toMatchSnapshot( + 'invalid config, admin user' + ); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx new file mode 100644 index 00000000000..d97f54cf8d0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx @@ -0,0 +1,56 @@ +/* + * 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 { click } from 'sonar-ui-common/helpers/testUtils'; +import CreateProjectModeSelection, { + CreateProjectModeSelectionProps +} from '../CreateProjectModeSelection'; +import { CreateProjectModes } from '../types'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ loadingBindings: true })).toMatchSnapshot('loading bbs instances'); + expect(shallowRender({ bbsBindingCount: 0 })).toMatchSnapshot('no bbs instances'); + expect(shallowRender({ bbsBindingCount: 2 })).toMatchSnapshot('too many bbs instances'); +}); + +it('should correctly pass the selected mode up', () => { + const onSelectMode = jest.fn(); + const wrapper = shallowRender({ onSelectMode }); + + click(wrapper.find('button.create-project-mode-type-manual')); + expect(onSelectMode).toBeCalledWith(CreateProjectModes.Manual); + + click(wrapper.find('button.create-project-mode-type-bbs')); + expect(onSelectMode).toBeCalledWith(CreateProjectModes.BitbucketServer); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageHeader-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageHeader-test.tsx new file mode 100644 index 00000000000..8c4c7fb8d41 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageHeader-test.tsx @@ -0,0 +1,33 @@ +/* + * 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 CreateProjectPageHeader, { CreateProjectPageHeaderProps } from '../CreateProjectPageHeader'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ showBreadcrumb: true })).toMatchSnapshot('with breadcrumb'); + expect(shallowRender({ additionalActions: 'Bar' })).toMatchSnapshot('additional content'); +}); + +function shallowRender(props: Partial = {}) { + return shallow(); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageSonarQube-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageSonarQube-test.tsx new file mode 100644 index 00000000000..dbc4aeff690 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageSonarQube-test.tsx @@ -0,0 +1,85 @@ +/* + * 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 { addWhitePageClass } from 'sonar-ui-common/helpers/pages'; +import { getAlmSettings } from '../../../../api/almSettings'; +import { mockLocation, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks'; +import { ALM_KEYS } from '../../../../types/alm-settings'; +import { CreateProjectPageSonarQube } from '../CreateProjectPageSonarQube'; +import { CreateProjectModes } from '../types'; + +jest.mock('../../../../api/almSettings', () => ({ + getAlmSettings: jest.fn().mockResolvedValue([{ alm: ALM_KEYS.BITBUCKET, key: 'foo' }]) +})); + +jest.mock('sonar-ui-common/helpers/pages', () => ({ + addWhitePageClass: jest.fn(), + removeWhitePageClass: jest.fn() +})); + +beforeEach(jest.clearAllMocks); + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); + expect(getAlmSettings).toBeCalled(); +}); + +it('should render correctly if no branch support', () => { + expect(shallowRender({ appState: { branchesEnabled: false } })).toMatchSnapshot(); + expect(getAlmSettings).not.toBeCalled(); +}); + +it('should render correctly if the manual method is selected', () => { + const push = jest.fn(); + const location = { query: { mode: CreateProjectModes.Manual } }; + const wrapper = shallowRender({ router: mockRouter({ push }) }); + + wrapper.instance().handleModeSelect(CreateProjectModes.Manual); + expect(push).toBeCalledWith(expect.objectContaining(location)); + + expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot(); + expect(addWhitePageClass).toBeCalled(); +}); + +it('should render correctly if the BBS method is selected', () => { + const push = jest.fn(); + const location = { query: { mode: CreateProjectModes.BitbucketServer } }; + const wrapper = shallowRender({ router: mockRouter({ push }) }); + + wrapper.instance().handleModeSelect(CreateProjectModes.BitbucketServer); + expect(push).toBeCalledWith(expect.objectContaining(location)); + + expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot(); + expect(addWhitePageClass).toBeCalled(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap new file mode 100644 index 00000000000..f46667b6a6d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap @@ -0,0 +1,440 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should correctly handle opening/closing accordions: 2nd opened 1`] = ` +
+
+ + collapse_all + +
+ + Project + + } + > +
+ + + Repo + + + + + +
+ + + Bar + + +
+ + onboarding.create_project.repository_imported + +
+
+
+
+ + Project 2 + + } + > +
+ + onboarding.create_project.no_bbs_repos + +
+
+
+`; + +exports[`should render correctly: default 1`] = ` +
+
+ + expand_all + +
+ + Project + + } + > +
+ + + Repo + + + + + +
+ + + Bar + + +
+ + onboarding.create_project.repository_imported + +
+
+
+
+ + Project 2 + + } + /> +
+`; + +exports[`should render correctly: importing 1`] = ` +
+
+ + expand_all + +
+ + Project + + } + > +
+ + + Repo + + + + + +
+ + + Bar + + +
+ + onboarding.create_project.repository_imported + +
+
+
+
+ + Project 2 + + } + /> +
+`; + +exports[`should render correctly: no projects 1`] = ` + + onboarding.create_project.no_bbs_projects + +`; + +exports[`should render correctly: no repos 1`] = ` +
+
+ + expand_all + +
+ + Project + + } + > +
+ + onboarding.create_project.no_bbs_repos + +
+
+ + Project 2 + + } + /> +
+`; + +exports[`should render correctly: selected repo 1`] = ` +
+
+ + expand_all + +
+ + Project + + } + > +
+ + + Repo + + + + + +
+ + + Bar + + +
+ + onboarding.create_project.repository_imported + +
+
+
+
+ + Project 2 + + } + /> +
+`; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap new file mode 100644 index 00000000000..f7dd160502f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap @@ -0,0 +1,225 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +
+
+

+ onboarding.create_project.grant_access_to_bbs.title +

+

+ onboarding.create_project.grant_access_to_bbs.help +

+ + + + + save + + + + +

+ onboarding.create_project.pat_help.title +

+

+ onboarding.create_project.pat_help.bbs_help_1 +

+ +

+ onboarding.create_project.pat_help.bbs_help_2 +

+
    +
  • + + onboarding.create_project.pat_help.read_permission + , + } + } + /> +
  • +
  • + + onboarding.create_project.pat_help.read_permission + , + } + } + /> +
  • +
+
+
+`; + +exports[`should render correctly: submitting 1`] = ` +
+
+

+ onboarding.create_project.grant_access_to_bbs.title +

+

+ onboarding.create_project.grant_access_to_bbs.help +

+ + + + + save + + + + +

+ onboarding.create_project.pat_help.title +

+

+ onboarding.create_project.pat_help.bbs_help_1 +

+ +

+ onboarding.create_project.pat_help.bbs_help_2 +

+
    +
  • + + onboarding.create_project.pat_help.read_permission + , + } + } + /> +
  • +
  • + + onboarding.create_project.pat_help.read_permission + , + } + } + /> +
  • +
+
+
+`; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap new file mode 100644 index 00000000000..94370ff8cc3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap new file mode 100644 index 00000000000..f887af050b0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap @@ -0,0 +1,383 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` + + + + +
+ } + showBreadcrumb={true} + title={ + + + onboarding.create_project.from_bbs + + } + /> + + +`; + +exports[`should render correctly: importing 1`] = ` + + + + + + } + showBreadcrumb={true} + title={ + + + onboarding.create_project.from_bbs + + } + /> + + +`; + +exports[`should render correctly: invalid config, admin user 1`] = ` + + + + + + } + showBreadcrumb={true} + title={ + + + onboarding.create_project.from_bbs + + } + /> + + + settings.page + , + } + } + /> + + +`; + +exports[`should render correctly: invalid config, regular user 1`] = ` + + + + + + } + showBreadcrumb={true} + title={ + + + onboarding.create_project.from_bbs + + } + /> + + onboarding.create_project.no_bbs_binding + + +`; + +exports[`should render correctly: loading 1`] = ` + + + + + + } + showBreadcrumb={true} + title={ + + + onboarding.create_project.from_bbs + + } + /> + + +`; + +exports[`should render correctly: pat form 1`] = ` + + + + onboarding.create_project.from_bbs + + } + /> + + +`; + +exports[`should render correctly: selected repo 1`] = ` + + + + + + } + showBreadcrumb={true} + title={ + + + onboarding.create_project.from_bbs + + } + /> + + +`; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap new file mode 100644 index 00000000000..4ccb9b5bc8f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap @@ -0,0 +1,267 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` + +
+

+ my_account.create_new.TRK +

+

+ onboarding.create_project.select_method +

+
+
+ + +
+
+`; + +exports[`should render correctly: loading bbs instances 1`] = ` + +
+

+ my_account.create_new.TRK +

+

+ onboarding.create_project.select_method +

+
+
+ + +
+
+`; + +exports[`should render correctly: no bbs instances 1`] = ` + +
+

+ my_account.create_new.TRK +

+

+ onboarding.create_project.select_method +

+
+
+ + +
+
+`; + +exports[`should render correctly: too many bbs instances 1`] = ` + +
+

+ my_account.create_new.TRK +

+

+ onboarding.create_project.select_method +

+
+
+ + +
+
+`; 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 3c461b88591..7757c69cf4d 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 @@ -41,7 +41,7 @@ exports[`should render correctly for SonarQube 1`] = ` - +

+ Foo +

+ Bar + +`; + +exports[`should render correctly: default 1`] = ` +
+

+ Foo +

+
+`; + +exports[`should render correctly: with breadcrumb 1`] = ` +
+

+ + my_account.create_new.TRK + + + Foo +

+
+`; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageSonarQube-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageSonarQube-test.tsx.snap new file mode 100644 index 00000000000..6018c82fd3a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageSonarQube-test.tsx.snap @@ -0,0 +1,114 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + +
+ +
+
+`; + +exports[`should render correctly if no branch support 1`] = ` + + +
+ +
+
+`; + +exports[`should render correctly if the BBS method is selected 1`] = ` + + +
+ +
+
+`; + +exports[`should render correctly if the manual method is selected 1`] = ` + + +
+ +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap index a68eafd3480..a0189b7cfd6 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap @@ -1,85 +1,90 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -
+ +
-
- - - - - - + + + + + + + set_up + + - - - set_up - - - + +
- + `; 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 90cfab82fc3..a57b40afced 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 @@ -17,6 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +#create-project { + padding-top: 0 !important; +} + +#create-project header { + padding-top: 20px; +} + +.white-page #create-project header { + background-color: white; + position: sticky; + top: var(--globalNavHeight); + z-index: var(--pageMainZIndex); +} + .create-project { display: flex !important; justify-content: space-between; @@ -124,3 +139,25 @@ .create-project-actions .icon-checkbox { margin-right: 8px; } + +.create-project-modes { + margin: 0 auto; + max-width: 500px; +} + +.create-project-import-bbs i.icon-radio { + flex-shrink: 0; +} + +.create-project-import-bbs .open .boxed-group-header { + border-bottom: 1px solid var(--barBorderColor); +} + +.create-project-import-bbs .boxed-group-inner { + padding-top: calc(3 * var(--gridSize)); +} + +.create-project-import-bbs-repo { + width: 250px; + min-height: 40px; +} diff --git a/server/sonar-web/src/main/js/apps/create/project/types.ts b/server/sonar-web/src/main/js/apps/create/project/types.ts new file mode 100644 index 00000000000..b316a4e781a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/types.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ +export enum CreateProjectModes { + Manual = 'manual', + BitbucketServer = 'bbs' +} diff --git a/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts b/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts index d664e6b3fc7..6f3320db435 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts @@ -18,12 +18,24 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { + AlmSettingsInstance, + ALM_KEYS, AzureBindingDefinition, BitbucketBindingDefinition, GithubBindingDefinition, GitlabBindingDefinition } from '../../types/alm-settings'; +export function mockAlmSettingsInstance( + overrides: Partial = {} +): AlmSettingsInstance { + return { + alm: ALM_KEYS.GITHUB, + key: 'key', + ...overrides + }; +} + export function mockAzureDefinition( overrides: Partial = {} ): AzureBindingDefinition { 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 a69905f209a..12ff218a47f 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -40,6 +40,7 @@ close=Close closed=Closed code=Code color=Color +collapse_all=Collapse all compare=Compare component=Component configure=Configure @@ -68,8 +69,9 @@ end_date=End Date edit=Edit events=Events example=Example -extend=Extend +expand_all=Expand all explore=Explore +extend=Extend false=False favorite=Favorite file=File @@ -3059,7 +3061,7 @@ onboarding.project_analysis.suggestions.bitbucket_extra=In case you need it, the onboarding.project_analysis.suggestions.github=If you are using Travis CI, the SonarCloud Travis Add-on makes it easier to run these commands with your CI process. onboarding.create_project.header=Analyze projects -onboarding.create_project.setup_manually=Set up manually +onboarding.create_project.setup_manually=Create manually onboarding.create_project.create_new_org=Create another organization onboarding.create_project.import_new_org=Import another organization onboarding.create_project.install_app_description.bitbucket=We need you to install the SonarCloud Bitbucket application on one of your team in order to select which repositories you want to analyze. @@ -3081,13 +3083,40 @@ onboarding.create_project.display_name=Display name onboarding.create_project.display_name.error=The display name is required. onboarding.create_project.display_name.description=Up to 255 characters onboarding.create_project.display_name.help=Some scanners might override the value you provide. -onboarding.create_project.repository_imported=Already imported: {link} +onboarding.create_project.repository_imported=Already set up onboarding.create_project.see_project=See the project onboarding.create_project.select_repositories=Select repositories onboarding.create_project.select_all_repositories=Select all available repositories onboarding.create_project.subscribe_to_import_private_repositories=You need to subscribe your organization to a paid plan to import private projects onboarding.create_project.encourage_to_subscribe=Subscribe your organization to our paid plan to get unlimited private projects. onboarding.create_project.subscribtion_success_x={0} has been successfully upgraded to paid plan. You can now import and analyze private projects. +onboarding.create_project.from_bbs=From Bitbucket Server +onboarding.create_project.grant_access_to_bbs.title=Grant access to your repositories +onboarding.create_project.grant_access_to_bbs.help=SonarQube needs a personal access token to access and list your repositories from Bitbucket Server. +onboarding.create_project.select_method=How do you want to create your project? +onboarding.create_project.select_method.manual=Manually +onboarding.create_project.select_method.from_bbs=From a Bitbucket Server repository +onboarding.create_project.check_bbs_supported=Checking if available +onboarding.create_project.too_many_bbs_instances_X=You must have exactly 1 Bitbucket Server instance configured in order to use this method. You currently have {0}. +onboarding.create_project.zero_bbs_instances=You must first configure a Bitbucket Server instance. +onboarding.create_project.bbs_not_configured=This feature isn't available +onboarding.create_project.no_bbs_binding=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method, but none were found. Either create the project manually, or contact your system administrator. +onboarding.create_project.no_bbs_binding.admin=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method. You can configure instances under {url}. +onboarding.create_project.enter_pat=Enter personal access token +onboarding.create_project.pat_help.title=How to create a personal access token? +onboarding.create_project.pat_help.bbs_help_1=Click the following link to generate a token in Bitbucket Server, and copy-paste it into the personal access token field. +onboarding.create_project.pat_help.bbs_help_2=Set a name, for example "SonarQube", and select the following permissions: +onboarding.create_project.pat_help.link=Create personal access token +onboarding.create_project.pat_help.bbs_permission_projects=Projects: {perm} +onboarding.create_project.pat_help.bbs_permission_repos=Repositories: {perm} +onboarding.create_project.pat_help.read_permission=Read +onboarding.create_project.error_fetching_bbs_projects=There was an error fetching the projects from Bitbucket Server. Contact your system administrator, or check your personal access token. +onboarding.create_project.error_fetching_bbs_repos=There was an error fetching the repositories from Bitbucket Server. Contact your system administrator, or check your personal access token. +onboarding.create_project.no_bbs_projects=No projects could be fetched from Bitbucket Server. Contact your system administrator, or check your personal access token. +onboarding.create_project.no_bbs_repos=No repositories were found for this project. Contact your system administrator, or check your personal access token. +onboarding.create_project.no_bbs_repos.filter=No repositories match your filter. +onboarding.create_project.import_selected_repo=Set up selected repository +onboarding.create_project.go_to_project=Go to project onboarding.create_organization.page.header=Create Organization onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects. -- 2.39.5