diff options
author | Shane Findley <shane.findley@sonarsource.com> | 2024-04-24 13:37:36 +0200 |
---|---|---|
committer | Matteo Mara <matteo.mara@sonarsource.com> | 2024-04-30 10:59:02 +0200 |
commit | 508bdf5808b401dd0745c0b01c4a8c5ad87bb307 (patch) | |
tree | 1fb10aefc1fd7b7affe7405372c9030162e17b69 /server/sonar-web/src/main/js/apps/create/project/BitbucketCloud | |
parent | 4b8b908e0e7aa880b6931ee2df3a88b95e9258a2 (diff) | |
download | sonarqube-508bdf5808b401dd0745c0b01c4a8c5ad87bb307.tar.gz sonarqube-508bdf5808b401dd0745c0b01c4a8c5ad87bb307.zip |
SONAR-21824 Bitbucket monorepo import functionality (#11005)
Diffstat (limited to 'server/sonar-web/src/main/js/apps/create/project/BitbucketCloud')
4 files changed, 242 insertions, 241 deletions
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudPersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudPersonalAccessTokenForm.tsx index 4525e18b453..194fdc6ca8b 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudPersonalAccessTokenForm.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudPersonalAccessTokenForm.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Link, Spinner } from '@sonarsource/echoes-react'; import { ButtonPrimary, FlagErrorIcon, @@ -24,17 +25,15 @@ import { FormField, InputField, LightPrimary, - Link, - Spinner, } from 'design-system'; import React from 'react'; import { FormattedMessage } from 'react-intl'; import { translate } from '../../../../helpers/l10n'; -import { AlmSettingsInstance } from '../../../../types/alm-settings'; +import { AlmInstanceBase } from '../../../../types/alm-settings'; import { usePersonalAccessToken } from '../usePersonalAccessToken'; interface Props { - almSetting: AlmSettingsInstance; + almSetting: AlmInstanceBase; resetPat: boolean; onPersonalAccessTokenCreated: () => void; } @@ -43,7 +42,7 @@ export default function BitbucketCloudPersonalAccessTokenForm({ almSetting, resetPat, onPersonalAccessTokenCreated, -}: Props) { +}: Readonly<Props>) { const { username, password, @@ -59,12 +58,12 @@ export default function BitbucketCloudPersonalAccessTokenForm({ } = usePersonalAccessToken(almSetting, resetPat, onPersonalAccessTokenCreated); if (checkingPat) { - return <Spinner className="sw-ml-2" loading />; + return <Spinner className="sw-ml-2" isLoading />; } const isInvalid = validationFailed && !touched; const canSubmit = Boolean(password) && Boolean(username); - const submitButtonDiabled = isInvalid || submitting || !canSubmit; + const submitButtonDisabled = isInvalid || submitting || !canSubmit; const errorMessage = validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect.bitbucket_cloud'); @@ -175,10 +174,10 @@ export default function BitbucketCloudPersonalAccessTokenForm({ </FlagMessage> </div> - <ButtonPrimary type="submit" disabled={submitButtonDiabled} className="sw-mb-6"> + <ButtonPrimary type="submit" disabled={submitButtonDisabled} className="sw-mb-6"> {translate('save')} </ButtonPrimary> - <Spinner className="sw-ml-2" loading={submitting} /> + <Spinner className="sw-ml-2" isLoading={submitting} /> </form> ); } diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx index 958473b2c7e..bb008260269 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx @@ -17,242 +17,215 @@ * 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 { LabelValueSelectOption } from 'design-system'; +import React, { useCallback, useMemo, useState } from 'react'; import { searchForBitbucketCloudRepositories } from '../../../../api/alm-integrations'; -import { Location, Router } from '../../../../components/hoc/withRouter'; +import { useLocation } from '../../../../components/hoc/withRouter'; import { BitbucketCloudRepository } from '../../../../types/alm-integration'; -import { AlmSettingsInstance } from '../../../../types/alm-settings'; -import { Paging } from '../../../../types/types'; +import { AlmKeys } from '../../../../types/alm-settings'; +import { DopSetting } from '../../../../types/dop-translation'; import { ImportProjectParam } from '../CreateProjectPage'; -import { BITBUCKET_CLOUD_PROJECTS_PAGESIZE } from '../constants'; +import { REPOSITORY_PAGE_SIZE } from '../constants'; +import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate'; import { CreateProjectModes } from '../types'; +import { useProjectCreate } from '../useProjectCreate'; +import { useProjectRepositorySearch } from '../useProjectRepositorySearch'; +import BitbucketCloudPersonalAccessTokenForm from './BitbucketCloudPersonalAccessTokenForm'; import BitbucketCloudProjectCreateRenderer from './BitbucketCloudProjectCreateRender'; interface Props { - canAdmin: boolean; - almInstances: AlmSettingsInstance[]; - loadingBindings: boolean; - location: Location; - router: Router; + dopSettings: DopSetting[]; + isLoadingBindings: boolean; onProjectSetupDone: (importProjects: ImportProjectParam) => void; } -interface State { - isLastPage?: boolean; - loading: boolean; - loadingMore: boolean; - projectsPaging: Omit<Paging, 'total'>; - resetPat: boolean; - repositories: BitbucketCloudRepository[]; - searching: boolean; - searchQuery: string; - selectedAlmInstance: AlmSettingsInstance; - showPersonalAccessTokenForm: boolean; -} - -export default class BitbucketCloudProjectCreate extends React.PureComponent<Props, State> { - 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. - loading: false, - loadingMore: false, - resetPat: false, - projectsPaging: { pageIndex: 1, pageSize: BITBUCKET_CLOUD_PROJECTS_PAGESIZE }, - repositories: [], - searching: false, - searchQuery: '', - selectedAlmInstance: props.almInstances[0], - showPersonalAccessTokenForm: true, - }; - } - - componentDidMount() { - this.mounted = true; - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.almInstances.length === 0 && this.props.almInstances.length > 0) { - this.setState({ selectedAlmInstance: this.props.almInstances[0] }, () => { - this.fetchData().catch(() => { - /* noop */ - }); - }); - } - } +export default function BitbucketCloudProjectCreate(props: Readonly<Props>) { + const { dopSettings, isLoadingBindings, onProjectSetupDone } = props; + + const [isLastPage, setIsLastPage] = useState<boolean>(true); + const [projectsPaging, setProjectsPaging] = useState<{ pageIndex: number; pageSize: number }>({ + pageIndex: 1, + pageSize: REPOSITORY_PAGE_SIZE, + }); + + const { + handlePersonalAccessTokenCreated, + handleSelectRepository, + isInitialized, + isLoadingRepositories, + isLoadingMoreRepositories, + isMonorepoSetup, + onSelectedAlmInstanceChange, + onSelectDopSetting, + repositories, + resetLoading, + resetPersonalAccessToken, + searchQuery, + selectedDopSetting, + selectedRepository, + setIsInitialized, + setRepositories, + setResetPersonalAccessToken, + setSearchQuery, + setShowPersonalAccessTokenForm, + showPersonalAccessTokenForm, + } = useProjectCreate<BitbucketCloudRepository, undefined>( + AlmKeys.BitbucketCloud, + dopSettings, + ({ slug }) => slug, + REPOSITORY_PAGE_SIZE, + ); + + const location = useLocation(); + const repositoryOptions = useMemo(() => repositories?.map(transformToOption), [repositories]); + + const fetchRepositories = useCallback( + (_orgKey?: string, query = '', pageIndex = 1, more = false) => { + if (!selectedDopSetting || showPersonalAccessTokenForm) { + return Promise.resolve(); + } - handlePersonalAccessTokenCreated = () => { - this.cleanUrl(); + resetLoading(true, more); - this.setState({ loading: true, showPersonalAccessTokenForm: false }, () => { - this.fetchData() - .then(() => this.setState({ loading: false })) + // eslint-disable-next-line local-rules/no-api-imports + return searchForBitbucketCloudRepositories( + selectedDopSetting.key, + query, + REPOSITORY_PAGE_SIZE, + pageIndex, + ) + .then((result) => { + resetLoading(false, more); + + if (result) { + setIsLastPage(result.isLastPage); + setIsInitialized(true); + } + + if (result?.repositories) { + setRepositories( + more && repositories && repositories.length > 0 + ? [...repositories, ...result.repositories] + : result.repositories, + ); + } + }) .catch(() => { - /* noop */ + resetLoading(false, more); + setResetPersonalAccessToken(true); + setShowPersonalAccessTokenForm(true); }); - }); - }; - - cleanUrl = () => { - const { location, router } = this.props; - delete location.query.resetPat; - router.replace(location); - }; - - async fetchData(more = false) { - const { - selectedAlmInstance, - searchQuery, - projectsPaging: { pageIndex, pageSize }, + }, + [ + repositories, + resetLoading, + selectedDopSetting, showPersonalAccessTokenForm, - } = this.state; - if (selectedAlmInstance && !showPersonalAccessTokenForm) { - const { isLastPage, repositories } = await searchForBitbucketCloudRepositories( - selectedAlmInstance.key, - searchQuery, - pageSize, - pageIndex, - ).catch(() => { - this.handleError(); - return { isLastPage: undefined, repositories: undefined }; - }); - if (this.mounted && isLastPage !== undefined && repositories !== undefined) { - if (more) { - this.setState((state) => ({ - isLastPage, - repositories: [...state.repositories, ...repositories], - })); - } else { - this.setState({ isLastPage, repositories }); - } + setIsInitialized, + setIsLastPage, + setRepositories, + setResetPersonalAccessToken, + setShowPersonalAccessTokenForm, + ], + ); + + const handleLoadMore = useCallback(() => { + const page = projectsPaging.pageIndex + 1; + setProjectsPaging((paging) => ({ + pageIndex: page, + pageSize: paging.pageSize, + })); + + fetchRepositories(undefined, searchQuery, page, true); + }, [fetchRepositories, projectsPaging, searchQuery, setProjectsPaging]); + + const handleImportRepository = useCallback( + (repositorySlug: string) => { + if (selectedDopSetting) { + onProjectSetupDone({ + creationMode: CreateProjectModes.BitbucketCloud, + almSetting: selectedDopSetting.key, + monorepo: false, + projects: [{ repositorySlug }], + }); } - } - } - - handleError = () => { - if (this.mounted) { - this.setState({ - projectsPaging: { pageIndex: 1, pageSize: BITBUCKET_CLOUD_PROJECTS_PAGESIZE }, - repositories: [], - resetPat: true, - showPersonalAccessTokenForm: true, - }); - } - - return undefined; - }; - - handleSearch = (searchQuery: string) => { - this.setState( - { - searching: true, - projectsPaging: { pageIndex: 1, pageSize: BITBUCKET_CLOUD_PROJECTS_PAGESIZE }, - searchQuery, - }, - () => { - this.fetchData().then( - () => { - if (this.mounted) { - this.setState({ searching: false }); - } - }, - () => { - /* noop */ - }, - ); - }, - ); - }; - - handleLoadMore = () => { - this.setState( - (state) => ({ - loadingMore: true, - projectsPaging: { - pageIndex: state.projectsPaging.pageIndex + 1, - pageSize: state.projectsPaging.pageSize, - }, - }), - () => { - this.fetchData(true).then( - () => { - if (this.mounted) { - this.setState({ loadingMore: false }); + }, + [onProjectSetupDone, selectedDopSetting], + ); + + const { isSearching, onSearch } = useProjectRepositorySearch( + AlmKeys.BitbucketCloud, + fetchRepositories, + isInitialized, + selectedDopSetting, + undefined, + setSearchQuery, + showPersonalAccessTokenForm, + ); + + return isMonorepoSetup ? ( + <MonorepoProjectCreate + dopSettings={dopSettings} + error={false} + loadingBindings={isLoadingBindings} + loadingOrganizations={false} + loadingRepositories={isLoadingRepositories} + onProjectSetupDone={onProjectSetupDone} + onSearchRepositories={onSearch} + onSelectDopSetting={onSelectDopSetting} + onSelectRepository={handleSelectRepository} + personalAccessTokenComponent={ + !isLoadingRepositories && + selectedDopSetting && ( + <BitbucketCloudPersonalAccessTokenForm + almSetting={selectedDopSetting} + resetPat={resetPersonalAccessToken} + onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated} + /> + ) + } + repositoryOptions={repositoryOptions} + repositorySearchQuery={searchQuery} + selectedDopSetting={selectedDopSetting} + selectedRepository={selectedRepository ? transformToOption(selectedRepository) : undefined} + showPersonalAccessToken={showPersonalAccessTokenForm || Boolean(location.query.resetPat)} + /> + ) : ( + <BitbucketCloudProjectCreateRenderer + isLastPage={isLastPage} + selectedAlmInstance={ + selectedDopSetting + ? { + alm: selectedDopSetting.type, + key: selectedDopSetting.key, + url: selectedDopSetting.url, } - }, - () => { - /* noop */ - }, - ); - }, - ); - }; - - handleImport = (repositorySlug: string) => { - const { selectedAlmInstance } = this.state; - - if (selectedAlmInstance) { - this.props.onProjectSetupDone({ - creationMode: CreateProjectModes.BitbucketCloud, - almSetting: selectedAlmInstance.key, - monorepo: false, - projects: [ - { - repositorySlug, - }, - ], - }); - } - }; - - onSelectedAlmInstanceChange = (instance: AlmSettingsInstance) => { - this.setState({ - selectedAlmInstance: instance, - showPersonalAccessTokenForm: true, - resetPat: false, - searching: false, - searchQuery: '', - projectsPaging: { pageIndex: 1, pageSize: BITBUCKET_CLOUD_PROJECTS_PAGESIZE }, - }); - }; + : undefined + } + almInstances={dopSettings?.map((instance) => ({ + alm: instance.type, + key: instance.key, + url: instance.url, + }))} + loadingMore={isLoadingMoreRepositories} + loading={isLoadingRepositories || isLoadingBindings} + onImport={handleImportRepository} + onLoadMore={handleLoadMore} + onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated} + onSearch={onSearch} + onSelectedAlmInstanceChange={onSelectedAlmInstanceChange} + repositories={repositories} + searching={isSearching} + searchQuery={searchQuery} + resetPat={resetPersonalAccessToken || Boolean(location.query.resetPat)} + showPersonalAccessTokenForm={showPersonalAccessTokenForm || Boolean(location.query.resetPat)} + /> + ); +} - render() { - const { canAdmin, loadingBindings, location, almInstances } = this.props; - const { - isLastPage = true, - selectedAlmInstance, - loading, - loadingMore, - repositories, - showPersonalAccessTokenForm, - resetPat, - searching, - searchQuery, - } = this.state; - return ( - <BitbucketCloudProjectCreateRenderer - isLastPage={isLastPage} - selectedAlmInstance={selectedAlmInstance} - almInstances={almInstances} - canAdmin={canAdmin} - loadingMore={loadingMore} - loading={loading || loadingBindings} - onImport={this.handleImport} - onLoadMore={this.handleLoadMore} - onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated} - onSearch={this.handleSearch} - onSelectedAlmInstanceChange={this.onSelectedAlmInstanceChange} - repositories={repositories} - searching={searching} - searchQuery={searchQuery} - resetPat={resetPat || Boolean(location.query.resetPat)} - showPersonalAccessTokenForm={ - showPersonalAccessTokenForm || Boolean(location.query.resetPat) - } - /> - ); - } +function transformToOption({ + name, + slug, +}: BitbucketCloudRepository): LabelValueSelectOption<string> { + return { value: slug, label: name }; } diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreateRender.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreateRender.tsx index 5d36dd40b55..b38aaa8dadc 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreateRender.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreateRender.tsx @@ -17,19 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { LightPrimary, Spinner, Title } from 'design-system'; -import * as React from 'react'; +import { Link, Spinner } from '@sonarsource/echoes-react'; +import { LightPrimary, Title } from 'design-system'; +import React, { useContext } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; import { translate } from '../../../../helpers/l10n'; +import { queryToSearch } from '../../../../helpers/urls'; import { BitbucketCloudRepository } from '../../../../types/alm-integration'; import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings'; +import { Feature } from '../../../../types/features'; import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown'; import WrongBindingCountAlert from '../components/WrongBindingCountAlert'; +import { CreateProjectModes } from '../types'; import BitbucketCloudPersonalAccessTokenForm from './BitbucketCloudPersonalAccessTokenForm'; import BitbucketCloudSearchForm from './BitbucketCloudSearchForm'; export interface BitbucketCloudProjectCreateRendererProps { + almInstances: AlmSettingsInstance[]; isLastPage: boolean; - canAdmin?: boolean; loading: boolean; loadingMore: boolean; onImport: (repositorySlug: string) => void; @@ -41,19 +47,21 @@ export interface BitbucketCloudProjectCreateRendererProps { resetPat: boolean; searching: boolean; searchQuery: string; - showPersonalAccessTokenForm: boolean; - almInstances: AlmSettingsInstance[]; selectedAlmInstance?: AlmSettingsInstance; + showPersonalAccessTokenForm: boolean; } export default function BitbucketCloudProjectCreateRenderer( props: Readonly<BitbucketCloudProjectCreateRendererProps>, ) { + const isMonorepoSupported = useContext(AvailableFeaturesContext).includes( + Feature.MonoRepositoryPullRequestDecoration, + ); + const { almInstances, isLastPage, selectedAlmInstance, - canAdmin, loading, loadingMore, repositories, @@ -70,7 +78,28 @@ export default function BitbucketCloudProjectCreateRenderer( {translate('onboarding.create_project.bitbucketcloud.title')} </Title> <LightPrimary className="sw-body-sm"> - {translate('onboarding.create_project.bitbucketcloud.subtitle')} + {isMonorepoSupported ? ( + <FormattedMessage + id="onboarding.create_project.bitbucketcloud.subtitle.with_monorepo" + values={{ + monorepoSetupLink: ( + <Link + to={{ + pathname: '/projects/create', + search: queryToSearch({ + mode: CreateProjectModes.BitbucketCloud, + mono: true, + }), + }} + > + <FormattedMessage id="onboarding.create_project.subtitle_monorepo_setup_link" /> + </Link> + ), + }} + /> + ) : ( + <FormattedMessage id="onboarding.create_project.bitbucketcloud.subtitle" /> + )} </LightPrimary> </header> @@ -81,10 +110,10 @@ export default function BitbucketCloudProjectCreateRenderer( onChangeConfig={props.onSelectedAlmInstanceChange} /> - <Spinner loading={loading} /> + <Spinner isLoading={loading} /> - {!loading && !selectedAlmInstance && ( - <WrongBindingCountAlert alm={AlmKeys.BitbucketCloud} canAdmin={!!canAdmin} /> + {!loading && almInstances && almInstances.length === 0 && !selectedAlmInstance && ( + <WrongBindingCountAlert alm={AlmKeys.BitbucketCloud} /> )} {!loading && diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudSearchForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudSearchForm.tsx index 2b3fcd5c70e..35183531dd8 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudSearchForm.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudSearchForm.tsx @@ -26,7 +26,7 @@ import { getBaseUrl } from '../../../../helpers/system'; import { queryToSearch } from '../../../../helpers/urls'; import { BitbucketCloudRepository } from '../../../../types/alm-integration'; import AlmRepoItem from '../components/AlmRepoItem'; -import { BITBUCKET_CLOUD_PROJECTS_PAGESIZE } from '../constants'; +import { REPOSITORY_PAGE_SIZE } from '../constants'; import { CreateProjectModes } from '../types'; export interface BitbucketCloudSearchFormProps { @@ -112,7 +112,7 @@ export default function BitbucketCloudSearchForm(props: BitbucketCloudSearchForm count={repositories.length} // we don't know the total, so only provide when we've reached the last page total={isLastPage ? repositories.length : undefined} - pageSize={BITBUCKET_CLOUD_PROJECTS_PAGESIZE} + pageSize={REPOSITORY_PAGE_SIZE} loadMore={props.onLoadMore} loading={loadingMore} /> |