* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { LabelValueSelectOption } from 'design-system/lib'; | |||||
import { LabelValueSelectOption } from 'design-system'; | |||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'; | import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
import { GroupBase } from 'react-select'; | import { GroupBase } from 'react-select'; | ||||
import { | import { | ||||
import AzureCreateProjectRenderer from './AzureProjectCreateRenderer'; | import AzureCreateProjectRenderer from './AzureProjectCreateRenderer'; | ||||
interface Props { | interface Props { | ||||
canAdmin: boolean; | |||||
dopSettings: DopSetting[]; | dopSettings: DopSetting[]; | ||||
isLoadingBindings: boolean; | isLoadingBindings: boolean; | ||||
onProjectSetupDone: (importProjects: ImportProjectParam) => void; | onProjectSetupDone: (importProjects: ImportProjectParam) => void; | ||||
} | } | ||||
export default function AzureProjectCreate(props: Readonly<Props>) { | export default function AzureProjectCreate(props: Readonly<Props>) { | ||||
const { canAdmin, dopSettings, isLoadingBindings, onProjectSetupDone } = props; | |||||
const { dopSettings, isLoadingBindings, onProjectSetupDone } = props; | |||||
const [isLoading, setIsLoading] = useState(false); | const [isLoading, setIsLoading] = useState(false); | ||||
const [loadingRepositories, setLoadingRepositories] = useState<Dict<boolean>>({}); | const [loadingRepositories, setLoadingRepositories] = useState<Dict<boolean>>({}); | ||||
const [isSearching, setIsSearching] = useState(false); | const [isSearching, setIsSearching] = useState(false); | ||||
return isMonorepoSetup ? ( | return isMonorepoSetup ? ( | ||||
<MonorepoProjectCreate | <MonorepoProjectCreate | ||||
canAdmin={canAdmin} | |||||
dopSettings={dopSettings} | dopSettings={dopSettings} | ||||
error={false} | error={false} | ||||
loadingBindings={isLoadingBindings} | loadingBindings={isLoadingBindings} | ||||
) : ( | ) : ( | ||||
<AzureCreateProjectRenderer | <AzureCreateProjectRenderer | ||||
almInstances={almInstances} | almInstances={almInstances} | ||||
canAdmin={canAdmin} | |||||
loading={isLoading || isLoadingBindings} | loading={isLoading || isLoadingBindings} | ||||
loadingRepositories={loadingRepositories} | loadingRepositories={loadingRepositories} | ||||
onImportRepository={handleImportRepository} | onImportRepository={handleImportRepository} |
} from 'design-system'; | } from 'design-system'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||
import { useAppState } from '../../../../app/components/app-state/withAppStateContext'; | |||||
import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; | import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; | ||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { getGlobalSettingsUrl, queryToSearch } from '../../../../helpers/urls'; | import { getGlobalSettingsUrl, queryToSearch } from '../../../../helpers/urls'; | ||||
import AzureProjectsList from './AzureProjectsList'; | import AzureProjectsList from './AzureProjectsList'; | ||||
export interface AzureProjectCreateRendererProps { | export interface AzureProjectCreateRendererProps { | ||||
canAdmin?: boolean; | |||||
loading: boolean; | loading: boolean; | ||||
loadingRepositories: Dict<boolean>; | loadingRepositories: Dict<boolean>; | ||||
onImportRepository: (resository: AzureRepository) => void; | onImportRepository: (resository: AzureRepository) => void; | ||||
props: Readonly<AzureProjectCreateRendererProps>, | props: Readonly<AzureProjectCreateRendererProps>, | ||||
) { | ) { | ||||
const { | const { | ||||
canAdmin, | |||||
loading, | loading, | ||||
loadingRepositories, | loadingRepositories, | ||||
projects, | projects, | ||||
Feature.MonoRepositoryPullRequestDecoration, | Feature.MonoRepositoryPullRequestDecoration, | ||||
); | ); | ||||
const { canAdmin } = useAppState(); | |||||
const showCountError = !loading && (!almInstances || almInstances.length === 0); | const showCountError = !loading && (!almInstances || almInstances.length === 0); | ||||
const showUrlError = | const showUrlError = | ||||
!loading && selectedAlmInstance !== undefined && selectedAlmInstance.url === undefined; | !loading && selectedAlmInstance !== undefined && selectedAlmInstance.url === undefined; | ||||
</FlagMessage> | </FlagMessage> | ||||
)} | )} | ||||
{showCountError && <WrongBindingCountAlert alm={AlmKeys.Azure} canAdmin={!!canAdmin} />} | |||||
{showCountError && <WrongBindingCountAlert alm={AlmKeys.Azure} />} | |||||
{!loading && | {!loading && | ||||
selectedAlmInstance?.url && | selectedAlmInstance?.url && |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { Link, Spinner } from '@sonarsource/echoes-react'; | |||||
import { | import { | ||||
ButtonPrimary, | ButtonPrimary, | ||||
FlagErrorIcon, | FlagErrorIcon, | ||||
FormField, | FormField, | ||||
InputField, | InputField, | ||||
LightPrimary, | LightPrimary, | ||||
Link, | |||||
Spinner, | |||||
} from 'design-system'; | } from 'design-system'; | ||||
import React from 'react'; | import React from 'react'; | ||||
import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { AlmSettingsInstance } from '../../../../types/alm-settings'; | |||||
import { AlmInstanceBase } from '../../../../types/alm-settings'; | |||||
import { usePersonalAccessToken } from '../usePersonalAccessToken'; | import { usePersonalAccessToken } from '../usePersonalAccessToken'; | ||||
interface Props { | interface Props { | ||||
almSetting: AlmSettingsInstance; | |||||
almSetting: AlmInstanceBase; | |||||
resetPat: boolean; | resetPat: boolean; | ||||
onPersonalAccessTokenCreated: () => void; | onPersonalAccessTokenCreated: () => void; | ||||
} | } | ||||
almSetting, | almSetting, | ||||
resetPat, | resetPat, | ||||
onPersonalAccessTokenCreated, | onPersonalAccessTokenCreated, | ||||
}: Props) { | |||||
}: Readonly<Props>) { | |||||
const { | const { | ||||
username, | username, | ||||
password, | password, | ||||
} = usePersonalAccessToken(almSetting, resetPat, onPersonalAccessTokenCreated); | } = usePersonalAccessToken(almSetting, resetPat, onPersonalAccessTokenCreated); | ||||
if (checkingPat) { | if (checkingPat) { | ||||
return <Spinner className="sw-ml-2" loading />; | |||||
return <Spinner className="sw-ml-2" isLoading />; | |||||
} | } | ||||
const isInvalid = validationFailed && !touched; | const isInvalid = validationFailed && !touched; | ||||
const canSubmit = Boolean(password) && Boolean(username); | const canSubmit = Boolean(password) && Boolean(username); | ||||
const submitButtonDiabled = isInvalid || submitting || !canSubmit; | |||||
const submitButtonDisabled = isInvalid || submitting || !canSubmit; | |||||
const errorMessage = | const errorMessage = | ||||
validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect.bitbucket_cloud'); | validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect.bitbucket_cloud'); | ||||
</FlagMessage> | </FlagMessage> | ||||
</div> | </div> | ||||
<ButtonPrimary type="submit" disabled={submitButtonDiabled} className="sw-mb-6"> | |||||
<ButtonPrimary type="submit" disabled={submitButtonDisabled} className="sw-mb-6"> | |||||
{translate('save')} | {translate('save')} | ||||
</ButtonPrimary> | </ButtonPrimary> | ||||
<Spinner className="sw-ml-2" loading={submitting} /> | |||||
<Spinner className="sw-ml-2" isLoading={submitting} /> | |||||
</form> | </form> | ||||
); | ); | ||||
} | } |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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 { 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 { 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 { 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 { CreateProjectModes } from '../types'; | ||||
import { useProjectCreate } from '../useProjectCreate'; | |||||
import { useProjectRepositorySearch } from '../useProjectRepositorySearch'; | |||||
import BitbucketCloudPersonalAccessTokenForm from './BitbucketCloudPersonalAccessTokenForm'; | |||||
import BitbucketCloudProjectCreateRenderer from './BitbucketCloudProjectCreateRender'; | import BitbucketCloudProjectCreateRenderer from './BitbucketCloudProjectCreateRender'; | ||||
interface Props { | interface Props { | ||||
canAdmin: boolean; | |||||
almInstances: AlmSettingsInstance[]; | |||||
loadingBindings: boolean; | |||||
location: Location; | |||||
router: Router; | |||||
dopSettings: DopSetting[]; | |||||
isLoadingBindings: boolean; | |||||
onProjectSetupDone: (importProjects: ImportProjectParam) => void; | 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(() => { | .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, | 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 }; | |||||
} | } |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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 { translate } from '../../../../helpers/l10n'; | ||||
import { queryToSearch } from '../../../../helpers/urls'; | |||||
import { BitbucketCloudRepository } from '../../../../types/alm-integration'; | import { BitbucketCloudRepository } from '../../../../types/alm-integration'; | ||||
import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings'; | import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings'; | ||||
import { Feature } from '../../../../types/features'; | |||||
import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown'; | import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown'; | ||||
import WrongBindingCountAlert from '../components/WrongBindingCountAlert'; | import WrongBindingCountAlert from '../components/WrongBindingCountAlert'; | ||||
import { CreateProjectModes } from '../types'; | |||||
import BitbucketCloudPersonalAccessTokenForm from './BitbucketCloudPersonalAccessTokenForm'; | import BitbucketCloudPersonalAccessTokenForm from './BitbucketCloudPersonalAccessTokenForm'; | ||||
import BitbucketCloudSearchForm from './BitbucketCloudSearchForm'; | import BitbucketCloudSearchForm from './BitbucketCloudSearchForm'; | ||||
export interface BitbucketCloudProjectCreateRendererProps { | export interface BitbucketCloudProjectCreateRendererProps { | ||||
almInstances: AlmSettingsInstance[]; | |||||
isLastPage: boolean; | isLastPage: boolean; | ||||
canAdmin?: boolean; | |||||
loading: boolean; | loading: boolean; | ||||
loadingMore: boolean; | loadingMore: boolean; | ||||
onImport: (repositorySlug: string) => void; | onImport: (repositorySlug: string) => void; | ||||
resetPat: boolean; | resetPat: boolean; | ||||
searching: boolean; | searching: boolean; | ||||
searchQuery: string; | searchQuery: string; | ||||
showPersonalAccessTokenForm: boolean; | |||||
almInstances: AlmSettingsInstance[]; | |||||
selectedAlmInstance?: AlmSettingsInstance; | selectedAlmInstance?: AlmSettingsInstance; | ||||
showPersonalAccessTokenForm: boolean; | |||||
} | } | ||||
export default function BitbucketCloudProjectCreateRenderer( | export default function BitbucketCloudProjectCreateRenderer( | ||||
props: Readonly<BitbucketCloudProjectCreateRendererProps>, | props: Readonly<BitbucketCloudProjectCreateRendererProps>, | ||||
) { | ) { | ||||
const isMonorepoSupported = useContext(AvailableFeaturesContext).includes( | |||||
Feature.MonoRepositoryPullRequestDecoration, | |||||
); | |||||
const { | const { | ||||
almInstances, | almInstances, | ||||
isLastPage, | isLastPage, | ||||
selectedAlmInstance, | selectedAlmInstance, | ||||
canAdmin, | |||||
loading, | loading, | ||||
loadingMore, | loadingMore, | ||||
repositories, | repositories, | ||||
{translate('onboarding.create_project.bitbucketcloud.title')} | {translate('onboarding.create_project.bitbucketcloud.title')} | ||||
</Title> | </Title> | ||||
<LightPrimary className="sw-body-sm"> | <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> | </LightPrimary> | ||||
</header> | </header> | ||||
onChangeConfig={props.onSelectedAlmInstanceChange} | 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 && | {!loading && |
import { queryToSearch } from '../../../../helpers/urls'; | import { queryToSearch } from '../../../../helpers/urls'; | ||||
import { BitbucketCloudRepository } from '../../../../types/alm-integration'; | import { BitbucketCloudRepository } from '../../../../types/alm-integration'; | ||||
import AlmRepoItem from '../components/AlmRepoItem'; | import AlmRepoItem from '../components/AlmRepoItem'; | ||||
import { BITBUCKET_CLOUD_PROJECTS_PAGESIZE } from '../constants'; | |||||
import { REPOSITORY_PAGE_SIZE } from '../constants'; | |||||
import { CreateProjectModes } from '../types'; | import { CreateProjectModes } from '../types'; | ||||
export interface BitbucketCloudSearchFormProps { | export interface BitbucketCloudSearchFormProps { | ||||
count={repositories.length} | count={repositories.length} | ||||
// we don't know the total, so only provide when we've reached the last page | // we don't know the total, so only provide when we've reached the last page | ||||
total={isLastPage ? repositories.length : undefined} | total={isLastPage ? repositories.length : undefined} | ||||
pageSize={BITBUCKET_CLOUD_PROJECTS_PAGESIZE} | |||||
pageSize={REPOSITORY_PAGE_SIZE} | |||||
loadMore={props.onLoadMore} | loadMore={props.onLoadMore} | ||||
loading={loadingMore} | loading={loadingMore} | ||||
/> | /> |
import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer'; | import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer'; | ||||
interface Props { | interface Props { | ||||
canAdmin: boolean; | |||||
almInstances: AlmSettingsInstance[]; | almInstances: AlmSettingsInstance[]; | ||||
loadingBindings: boolean; | loadingBindings: boolean; | ||||
location: Location; | location: Location; | ||||
}; | }; | ||||
render() { | render() { | ||||
const { canAdmin, loadingBindings, location, almInstances } = this.props; | |||||
const { loadingBindings, location, almInstances } = this.props; | |||||
const { | const { | ||||
selectedAlmInstance, | selectedAlmInstance, | ||||
loading, | loading, | ||||
<BitbucketCreateProjectRenderer | <BitbucketCreateProjectRenderer | ||||
selectedAlmInstance={selectedAlmInstance} | selectedAlmInstance={selectedAlmInstance} | ||||
almInstances={almInstances} | almInstances={almInstances} | ||||
canAdmin={canAdmin} | |||||
loading={loading || loadingBindings} | loading={loading || loadingBindings} | ||||
onImportRepository={this.handleImportRepository} | onImportRepository={this.handleImportRepository} | ||||
onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated} | onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated} |
export interface BitbucketProjectCreateRendererProps { | export interface BitbucketProjectCreateRendererProps { | ||||
selectedAlmInstance?: AlmSettingsInstance; | selectedAlmInstance?: AlmSettingsInstance; | ||||
almInstances: AlmSettingsInstance[]; | almInstances: AlmSettingsInstance[]; | ||||
canAdmin?: boolean; | |||||
loading: boolean; | loading: boolean; | ||||
onImportRepository: (repository: BitbucketRepository) => void; | onImportRepository: (repository: BitbucketRepository) => void; | ||||
onSearch: (query: string) => void; | onSearch: (query: string) => void; | ||||
const { | const { | ||||
almInstances, | almInstances, | ||||
selectedAlmInstance, | selectedAlmInstance, | ||||
canAdmin, | |||||
loading, | loading, | ||||
projects, | projects, | ||||
projectRepositories, | projectRepositories, | ||||
<Spinner loading={loading}> | <Spinner loading={loading}> | ||||
{!loading && !selectedAlmInstance && ( | {!loading && !selectedAlmInstance && ( | ||||
<WrongBindingCountAlert alm={AlmKeys.BitbucketServer} canAdmin={!!canAdmin} /> | |||||
<WrongBindingCountAlert alm={AlmKeys.BitbucketServer} /> | |||||
)} | )} | ||||
{!loading && | {!loading && |
import * as React from 'react'; | import * as React from 'react'; | ||||
import { Helmet } from 'react-helmet-async'; | import { Helmet } from 'react-helmet-async'; | ||||
import { getDopSettings } from '../../../api/dop-translation'; | import { getDopSettings } from '../../../api/dop-translation'; | ||||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | |||||
import withAvailableFeatures, { | import withAvailableFeatures, { | ||||
WithAvailableFeaturesProps, | WithAvailableFeaturesProps, | ||||
} from '../../../app/components/available-features/withAvailableFeatures'; | } from '../../../app/components/available-features/withAvailableFeatures'; | ||||
import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; | import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; | import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { DopSetting } from '../../../types/dop-translation'; | import { DopSetting } from '../../../types/dop-translation'; | ||||
import { Feature } from '../../../types/features'; | import { Feature } from '../../../types/features'; | ||||
import AlmBindingDefinitionForm from '../../settings/components/almIntegration/AlmBindingDefinitionForm'; | import AlmBindingDefinitionForm from '../../settings/components/almIntegration/AlmBindingDefinitionForm'; | ||||
import { CreateProjectModes } from './types'; | import { CreateProjectModes } from './types'; | ||||
export interface CreateProjectPageProps extends WithAvailableFeaturesProps { | export interface CreateProjectPageProps extends WithAvailableFeaturesProps { | ||||
appState: AppState; | |||||
location: Location; | location: Location; | ||||
router: Router; | router: Router; | ||||
} | } | ||||
interface State { | interface State { | ||||
azureSettings: DopSetting[]; | azureSettings: DopSetting[]; | ||||
bitbucketSettings: AlmSettingsInstance[]; | bitbucketSettings: AlmSettingsInstance[]; | ||||
bitbucketCloudSettings: AlmSettingsInstance[]; | |||||
bitbucketCloudSettings: DopSetting[]; | |||||
githubSettings: DopSetting[]; | githubSettings: DopSetting[]; | ||||
gitlabSettings: DopSetting[]; | gitlabSettings: DopSetting[]; | ||||
loading: boolean; | loading: boolean; | ||||
bitbucketSettings: dopSettings | bitbucketSettings: dopSettings | ||||
.filter(({ type }) => type === AlmKeys.BitbucketServer) | .filter(({ type }) => type === AlmKeys.BitbucketServer) | ||||
.map(({ key, type, url }) => ({ alm: type, key, url })), | .map(({ key, type, url }) => ({ alm: type, key, url })), | ||||
bitbucketCloudSettings: dopSettings | |||||
.filter(({ type }) => type === AlmKeys.BitbucketCloud) | |||||
.map(({ key, type, url }) => ({ alm: type, key, url })), | |||||
bitbucketCloudSettings: dopSettings.filter(({ type }) => type === AlmKeys.BitbucketCloud), | |||||
githubSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitHub), | githubSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitHub), | ||||
gitlabSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitLab), | gitlabSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitLab), | ||||
loading: false, | loading: false, | ||||
}; | }; | ||||
renderProjectCreation(mode?: CreateProjectModes) { | renderProjectCreation(mode?: CreateProjectModes) { | ||||
const { | |||||
appState: { canAdmin }, | |||||
location, | |||||
router, | |||||
} = this.props; | |||||
const { location, router } = this.props; | |||||
const { | const { | ||||
azureSettings, | azureSettings, | ||||
bitbucketSettings, | bitbucketSettings, | ||||
case CreateProjectModes.AzureDevOps: { | case CreateProjectModes.AzureDevOps: { | ||||
return ( | return ( | ||||
<AzureProjectCreate | <AzureProjectCreate | ||||
canAdmin={!!canAdmin} | |||||
dopSettings={azureSettings} | dopSettings={azureSettings} | ||||
isLoadingBindings={loading} | isLoadingBindings={loading} | ||||
onProjectSetupDone={this.handleProjectSetupDone} | onProjectSetupDone={this.handleProjectSetupDone} | ||||
case CreateProjectModes.BitbucketServer: { | case CreateProjectModes.BitbucketServer: { | ||||
return ( | return ( | ||||
<BitbucketProjectCreate | <BitbucketProjectCreate | ||||
canAdmin={!!canAdmin} | |||||
almInstances={bitbucketSettings} | almInstances={bitbucketSettings} | ||||
loadingBindings={loading} | loadingBindings={loading} | ||||
location={location} | location={location} | ||||
case CreateProjectModes.BitbucketCloud: { | case CreateProjectModes.BitbucketCloud: { | ||||
return ( | return ( | ||||
<BitbucketCloudProjectCreate | <BitbucketCloudProjectCreate | ||||
canAdmin={!!canAdmin} | |||||
loadingBindings={loading} | |||||
location={location} | |||||
dopSettings={bitbucketCloudSettings} | |||||
isLoadingBindings={loading} | |||||
onProjectSetupDone={this.handleProjectSetupDone} | onProjectSetupDone={this.handleProjectSetupDone} | ||||
router={router} | |||||
almInstances={bitbucketCloudSettings} | |||||
/> | /> | ||||
); | ); | ||||
} | } | ||||
case CreateProjectModes.GitHub: { | case CreateProjectModes.GitHub: { | ||||
return ( | return ( | ||||
<GitHubProjectCreate | <GitHubProjectCreate | ||||
canAdmin={!!canAdmin} | |||||
isLoadingBindings={loading} | isLoadingBindings={loading} | ||||
onProjectSetupDone={this.handleProjectSetupDone} | onProjectSetupDone={this.handleProjectSetupDone} | ||||
dopSettings={githubSettings} | dopSettings={githubSettings} | ||||
case CreateProjectModes.GitLab: { | case CreateProjectModes.GitLab: { | ||||
return ( | return ( | ||||
<GitlabProjectCreate | <GitlabProjectCreate | ||||
canAdmin={!!canAdmin} | |||||
dopSettings={gitlabSettings} | dopSettings={gitlabSettings} | ||||
isLoadingBindings={loading} | isLoadingBindings={loading} | ||||
onProjectSetupDone={this.handleProjectSetupDone} | onProjectSetupDone={this.handleProjectSetupDone} | ||||
} | } | ||||
} | } | ||||
export default withRouter(withAvailableFeatures(withAppStateContext(CreateProjectPage))); | |||||
export default withRouter(withAvailableFeatures(CreateProjectPage)); |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { LabelValueSelectOption } from 'design-system'; | import { LabelValueSelectOption } from 'design-system'; | ||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'; | |||||
import { getGithubOrganizations, getGithubRepositories } from '../../../../api/alm-integrations'; | import { getGithubOrganizations, getGithubRepositories } from '../../../../api/alm-integrations'; | ||||
import { useLocation, useRouter } from '../../../../components/hoc/withRouter'; | import { useLocation, useRouter } from '../../../../components/hoc/withRouter'; | ||||
import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration'; | import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration'; | ||||
import { AlmSettingsInstance } from '../../../../types/alm-settings'; | |||||
import { AlmInstanceBase, AlmKeys } from '../../../../types/alm-settings'; | |||||
import { DopSetting } from '../../../../types/dop-translation'; | import { DopSetting } from '../../../../types/dop-translation'; | ||||
import { Paging } from '../../../../types/types'; | |||||
import { ImportProjectParam } from '../CreateProjectPage'; | import { ImportProjectParam } from '../CreateProjectPage'; | ||||
import { REPOSITORY_PAGE_SIZE } from '../constants'; | |||||
import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate'; | import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate'; | ||||
import { CreateProjectModes } from '../types'; | import { CreateProjectModes } from '../types'; | ||||
import { useProjectCreate } from '../useProjectCreate'; | |||||
import { useProjectRepositorySearch } from '../useProjectRepositorySearch'; | |||||
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer'; | import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer'; | ||||
import { redirectToGithub } from './utils'; | import { redirectToGithub } from './utils'; | ||||
interface Props { | interface Props { | ||||
canAdmin: boolean; | |||||
isLoadingBindings: boolean; | isLoadingBindings: boolean; | ||||
onProjectSetupDone: (importProjects: ImportProjectParam) => void; | onProjectSetupDone: (importProjects: ImportProjectParam) => void; | ||||
dopSettings: DopSetting[]; | dopSettings: DopSetting[]; | ||||
} | } | ||||
const REPOSITORY_PAGE_SIZE = 50; | |||||
const REPOSITORY_SEARCH_DEBOUNCE_TIME = 250; | |||||
export default function GitHubProjectCreate(props: Readonly<Props>) { | export default function GitHubProjectCreate(props: Readonly<Props>) { | ||||
const { canAdmin, dopSettings, isLoadingBindings, onProjectSetupDone } = props; | |||||
const { dopSettings, isLoadingBindings, onProjectSetupDone } = props; | |||||
const repositorySearchDebounceId = useRef<NodeJS.Timeout | undefined>(); | |||||
const { | |||||
handleSelectRepository, | |||||
isInitialized, | |||||
isLoadingOrganizations, | |||||
isLoadingRepositories, | |||||
isMonorepoSetup, | |||||
onSelectedAlmInstanceChange, | |||||
onSelectDopSetting, | |||||
projectsPaging, | |||||
organizations, | |||||
repositories, | |||||
searchQuery, | |||||
selectedDopSetting, | |||||
selectedRepository, | |||||
setIsInitialized, | |||||
setIsLoadingRepositories, | |||||
setProjectsPaging, | |||||
setOrganizations, | |||||
setRepositories, | |||||
setSearchQuery, | |||||
setSelectedOrganization, | |||||
selectedOrganization, | |||||
setIsLoadingOrganizations, | |||||
} = useProjectCreate<GithubRepository, GithubOrganization>( | |||||
AlmKeys.GitHub, | |||||
dopSettings, | |||||
({ key }) => key, | |||||
REPOSITORY_PAGE_SIZE, | |||||
); | |||||
const [isInError, setIsInError] = useState(false); | const [isInError, setIsInError] = useState(false); | ||||
const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(true); | |||||
const [isLoadingRepositories, setIsLoadingRepositories] = useState(false); | |||||
const [organizations, setOrganizations] = useState<GithubOrganization[]>([]); | |||||
const [repositories, setRepositories] = useState<GithubRepository[]>([]); | |||||
const [repositoryPaging, setRepositoryPaging] = useState<Paging>({ | |||||
pageSize: REPOSITORY_PAGE_SIZE, | |||||
total: 0, | |||||
pageIndex: 1, | |||||
}); | |||||
const [searchQuery, setSearchQuery] = useState(''); | |||||
const [selectedDopSetting, setSelectedDopSetting] = useState<DopSetting>(); | |||||
const [selectedOrganization, setSelectedOrganization] = useState<GithubOrganization>(); | |||||
const [selectedRepository, setSelectedRepository] = useState<GithubRepository>(); | |||||
const [isAuthenticated, setIsAuthenticated] = useState(false); | |||||
const location = useLocation(); | const location = useLocation(); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
const isMonorepoSetup = location.query?.mono === 'true'; | |||||
const hasDopSettings = Boolean(dopSettings?.length); | |||||
const organizationOptions = useMemo(() => { | const organizationOptions = useMemo(() => { | ||||
return organizations.map(transformToOption); | return organizations.map(transformToOption); | ||||
}, [organizations]); | }, [organizations]); | ||||
}, [repositories]); | }, [repositories]); | ||||
const fetchRepositories = useCallback( | const fetchRepositories = useCallback( | ||||
async (params: { organizationKey: string; page?: number; query?: string }) => { | |||||
const { organizationKey, page = 1, query } = params; | |||||
(orgKey: string, query?: string, pageIndex = 1) => { | |||||
if (selectedDopSetting === undefined) { | if (selectedDopSetting === undefined) { | ||||
setIsInError(true); | setIsInError(true); | ||||
return; | |||||
return Promise.resolve(); | |||||
} | } | ||||
setIsLoadingRepositories(true); | setIsLoadingRepositories(true); | ||||
try { | |||||
const { paging, repositories } = await getGithubRepositories({ | |||||
almSetting: selectedDopSetting.key, | |||||
organization: organizationKey, | |||||
pageSize: REPOSITORY_PAGE_SIZE, | |||||
page, | |||||
query, | |||||
return getGithubRepositories({ | |||||
almSetting: selectedDopSetting.key, | |||||
organization: orgKey, | |||||
pageSize: REPOSITORY_PAGE_SIZE, | |||||
page: pageIndex, | |||||
query, | |||||
}) | |||||
.then(({ paging, repositories }) => { | |||||
setProjectsPaging(paging); | |||||
setRepositories((prevRepositories) => | |||||
pageIndex === 1 ? repositories : [...prevRepositories, ...repositories], | |||||
); | |||||
setIsInitialized(true); | |||||
}) | |||||
.finally(() => { | |||||
setIsLoadingRepositories(false); | |||||
}) | |||||
.catch(() => { | |||||
setProjectsPaging({ pageIndex: 1, pageSize: REPOSITORY_PAGE_SIZE, total: 0 }); | |||||
setRepositories([]); | |||||
}); | }); | ||||
}, | |||||
[ | |||||
selectedDopSetting, | |||||
setIsInitialized, | |||||
setIsLoadingRepositories, | |||||
setProjectsPaging, | |||||
setRepositories, | |||||
], | |||||
); | |||||
setRepositoryPaging(paging); | |||||
setRepositories((prevRepositories) => | |||||
page === 1 ? repositories : [...prevRepositories, ...repositories], | |||||
); | |||||
} catch (_) { | |||||
setRepositoryPaging({ pageIndex: 1, pageSize: REPOSITORY_PAGE_SIZE, total: 0 }); | |||||
setRepositories([]); | |||||
} finally { | |||||
setIsLoadingRepositories(false); | |||||
} | |||||
const onSelectDopSettingReauthenticate = useCallback( | |||||
(setting?: DopSetting) => { | |||||
onSelectDopSetting(setting); | |||||
setIsAuthenticated(false); | |||||
}, | |||||
[onSelectDopSetting], | |||||
); | |||||
const onSelectAlmSettingReauthenticate = useCallback( | |||||
(setting?: AlmInstanceBase) => { | |||||
onSelectedAlmInstanceChange(setting); | |||||
setIsAuthenticated(false); | |||||
}, | }, | ||||
[selectedDopSetting], | |||||
[onSelectedAlmInstanceChange], | |||||
); | ); | ||||
const handleImportRepository = useCallback( | const handleImportRepository = useCallback( | ||||
const handleLoadMore = useCallback(() => { | const handleLoadMore = useCallback(() => { | ||||
if (selectedOrganization) { | if (selectedOrganization) { | ||||
fetchRepositories({ | |||||
organizationKey: selectedOrganization.key, | |||||
page: repositoryPaging.pageIndex + 1, | |||||
query: searchQuery, | |||||
}); | |||||
fetchRepositories(selectedOrganization.key, searchQuery, projectsPaging.pageIndex + 1); | |||||
} | } | ||||
}, [fetchRepositories, repositoryPaging.pageIndex, searchQuery, selectedOrganization]); | |||||
}, [fetchRepositories, projectsPaging.pageIndex, searchQuery, selectedOrganization]); | |||||
const handleSelectOrganization = useCallback( | const handleSelectOrganization = useCallback( | ||||
(organizationKey: string) => { | (organizationKey: string) => { | ||||
setSearchQuery(''); | setSearchQuery(''); | ||||
setSelectedOrganization(organizations.find(({ key }) => key === organizationKey)); | setSelectedOrganization(organizations.find(({ key }) => key === organizationKey)); | ||||
fetchRepositories({ organizationKey }); | |||||
}, | |||||
[fetchRepositories, organizations], | |||||
); | |||||
const handleSelectRepository = useCallback( | |||||
(repositoryIdentifier: string) => { | |||||
setSelectedRepository(repositories.find(({ key }) => key === repositoryIdentifier)); | |||||
}, | |||||
[repositories], | |||||
); | |||||
const authenticateToGithub = useCallback(async () => { | |||||
try { | |||||
await redirectToGithub({ isMonorepoSetup, selectedDopSetting }); | |||||
} catch { | |||||
setIsInError(true); | |||||
} | |||||
}, [isMonorepoSetup, selectedDopSetting]); | |||||
const onSelectDopSetting = useCallback((setting: DopSetting | undefined) => { | |||||
setSelectedDopSetting(setting); | |||||
setOrganizations([]); | |||||
setRepositories([]); | |||||
setSearchQuery(''); | |||||
}, []); | |||||
const onSelectedAlmInstanceChange = useCallback( | |||||
(instance: AlmSettingsInstance) => { | |||||
onSelectDopSetting(dopSettings.find((dopSetting) => dopSetting.key === instance.key)); | |||||
}, | }, | ||||
[dopSettings, onSelectDopSetting], | |||||
[organizations, setSearchQuery, setSelectedOrganization], | |||||
); | ); | ||||
useEffect(() => { | |||||
const selectedDopSettingId = location.query?.dopSetting; | |||||
if (selectedDopSettingId !== undefined) { | |||||
const selectedDopSetting = dopSettings.find(({ id }) => id === selectedDopSettingId); | |||||
if (selectedDopSetting) { | |||||
setSelectedDopSetting(selectedDopSetting); | |||||
} | |||||
return; | |||||
} | |||||
if (dopSettings.length > 1) { | |||||
setSelectedDopSetting(undefined); | |||||
return; | |||||
} | |||||
setSelectedDopSetting(dopSettings[0]); | |||||
// eslint-disable-next-line react-hooks/exhaustive-deps | |||||
}, [hasDopSettings]); | |||||
useEffect(() => { | useEffect(() => { | ||||
if (selectedDopSetting?.url === undefined) { | if (selectedDopSetting?.url === undefined) { | ||||
setIsInError(true); | setIsInError(true); | ||||
setIsInError(false); | setIsInError(false); | ||||
const code = location.query?.code; | const code = location.query?.code; | ||||
if (code === undefined) { | |||||
authenticateToGithub().catch(() => { | |||||
setIsInError(true); | |||||
}); | |||||
} else { | |||||
delete location.query.code; | |||||
router.replace(location); | |||||
getGithubOrganizations(selectedDopSetting.key, code) | |||||
.then(({ organizations }) => { | |||||
setOrganizations(organizations); | |||||
setIsLoadingOrganizations(false); | |||||
}) | |||||
.catch(() => { | |||||
if (!isAuthenticated) { | |||||
if (code === undefined) { | |||||
redirectToGithub({ isMonorepoSetup, selectedDopSetting }).catch(() => { | |||||
setIsInError(true); | setIsInError(true); | ||||
}); | }); | ||||
} else { | |||||
setIsAuthenticated(true); | |||||
delete location.query.code; | |||||
router.replace(location); | |||||
getGithubOrganizations(selectedDopSetting.key, code) | |||||
.then(({ organizations }) => { | |||||
setOrganizations(organizations); | |||||
setIsLoadingOrganizations(false); | |||||
}) | |||||
.catch(() => { | |||||
setIsInError(true); | |||||
}); | |||||
} | |||||
} | } | ||||
// Disabling rule as it causes an infinite loop and should only be called for dopSetting changes. | |||||
// eslint-disable-next-line react-hooks/exhaustive-deps | // eslint-disable-next-line react-hooks/exhaustive-deps | ||||
}, [selectedDopSetting]); | }, [selectedDopSetting]); | ||||
useEffect(() => { | |||||
repositorySearchDebounceId.current = setTimeout(() => { | |||||
if (selectedOrganization) { | |||||
fetchRepositories({ | |||||
organizationKey: selectedOrganization.key, | |||||
query: searchQuery, | |||||
}); | |||||
} | |||||
}, REPOSITORY_SEARCH_DEBOUNCE_TIME); | |||||
return () => { | |||||
clearTimeout(repositorySearchDebounceId.current); | |||||
}; | |||||
// eslint-disable-next-line react-hooks/exhaustive-deps | |||||
}, [searchQuery]); | |||||
const { onSearch } = useProjectRepositorySearch( | |||||
AlmKeys.GitHub, | |||||
fetchRepositories, | |||||
isInitialized, | |||||
selectedDopSetting, | |||||
selectedOrganization?.key, | |||||
setSearchQuery, | |||||
); | |||||
return isMonorepoSetup ? ( | return isMonorepoSetup ? ( | ||||
<MonorepoProjectCreate | <MonorepoProjectCreate | ||||
dopSettings={dopSettings} | dopSettings={dopSettings} | ||||
canAdmin={canAdmin} | |||||
error={isInError} | error={isInError} | ||||
loadingBindings={isLoadingBindings} | loadingBindings={isLoadingBindings} | ||||
loadingOrganizations={isLoadingOrganizations} | loadingOrganizations={isLoadingOrganizations} | ||||
loadingRepositories={isLoadingRepositories} | loadingRepositories={isLoadingRepositories} | ||||
onProjectSetupDone={onProjectSetupDone} | onProjectSetupDone={onProjectSetupDone} | ||||
onSearchRepositories={setSearchQuery} | |||||
onSelectDopSetting={onSelectDopSetting} | |||||
onSearchRepositories={onSearch} | |||||
onSelectDopSetting={onSelectDopSettingReauthenticate} | |||||
onSelectOrganization={handleSelectOrganization} | onSelectOrganization={handleSelectOrganization} | ||||
onSelectRepository={handleSelectRepository} | onSelectRepository={handleSelectRepository} | ||||
organizationOptions={organizationOptions} | organizationOptions={organizationOptions} | ||||
key, | key, | ||||
url, | url, | ||||
}))} | }))} | ||||
canAdmin={canAdmin} | |||||
error={isInError} | error={isInError} | ||||
loadingBindings={isLoadingBindings} | loadingBindings={isLoadingBindings} | ||||
loadingOrganizations={isLoadingOrganizations} | loadingOrganizations={isLoadingOrganizations} | ||||
loadingRepositories={isLoadingRepositories} | loadingRepositories={isLoadingRepositories} | ||||
onImportRepository={handleImportRepository} | onImportRepository={handleImportRepository} | ||||
onLoadMore={handleLoadMore} | onLoadMore={handleLoadMore} | ||||
onSearch={setSearchQuery} | |||||
onSelectedAlmInstanceChange={onSelectedAlmInstanceChange} | |||||
onSearch={onSearch} | |||||
onSelectedAlmInstanceChange={onSelectAlmSettingReauthenticate} | |||||
onSelectOrganization={handleSelectOrganization} | onSelectOrganization={handleSelectOrganization} | ||||
organizations={organizations} | organizations={organizations} | ||||
repositories={repositories} | repositories={repositories} | ||||
repositoryPaging={repositoryPaging} | |||||
repositoryPaging={projectsPaging} | |||||
searchQuery={searchQuery} | searchQuery={searchQuery} | ||||
selectedAlmInstance={ | selectedAlmInstance={ | ||||
selectedDopSetting && { | selectedDopSetting && { |
import { DarkLabel, FlagMessage, InputSelect, LightPrimary, Title } from 'design-system'; | import { DarkLabel, FlagMessage, InputSelect, LightPrimary, Title } from 'design-system'; | ||||
import React, { useContext, useEffect, useState } from 'react'; | import React, { useContext, useEffect, useState } from 'react'; | ||||
import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||
import { useAppState } from '../../../../app/components/app-state/withAppStateContext'; | |||||
import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; | import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; | ||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { LabelValueSelectOption } from '../../../../helpers/search'; | import { LabelValueSelectOption } from '../../../../helpers/search'; | ||||
import { CreateProjectModes } from '../types'; | import { CreateProjectModes } from '../types'; | ||||
interface GitHubProjectCreateRendererProps { | interface GitHubProjectCreateRendererProps { | ||||
canAdmin: boolean; | |||||
error: boolean; | error: boolean; | ||||
loadingBindings: boolean; | loadingBindings: boolean; | ||||
loadingOrganizations: boolean; | loadingOrganizations: boolean; | ||||
); | ); | ||||
const { | const { | ||||
canAdmin, | |||||
error, | error, | ||||
loadingBindings, | loadingBindings, | ||||
loadingOrganizations, | loadingOrganizations, | ||||
repositories, | repositories, | ||||
} = props; | } = props; | ||||
const [selected, setSelected] = useState<Set<string>>(new Set()); | const [selected, setSelected] = useState<Set<string>>(new Set()); | ||||
const { canAdmin } = useAppState(); | |||||
useEffect(() => { | useEffect(() => { | ||||
const selectedKeys = Array.from(selected).filter((key) => | const selectedKeys = Array.from(selected).filter((key) => |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { LabelValueSelectOption } from 'design-system'; | import { LabelValueSelectOption } from 'design-system'; | ||||
import { orderBy } from 'lodash'; | |||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||||
import React, { useCallback, useMemo } from 'react'; | |||||
import { getGitlabProjects } from '../../../../api/alm-integrations'; | import { getGitlabProjects } from '../../../../api/alm-integrations'; | ||||
import { useLocation, useRouter } from '../../../../components/hoc/withRouter'; | |||||
import { useLocation } from '../../../../components/hoc/withRouter'; | |||||
import { GitlabProject } from '../../../../types/alm-integration'; | import { GitlabProject } from '../../../../types/alm-integration'; | ||||
import { AlmInstanceBase } from '../../../../types/alm-settings'; | |||||
import { AlmKeys } from '../../../../types/alm-settings'; | |||||
import { DopSetting } from '../../../../types/dop-translation'; | import { DopSetting } from '../../../../types/dop-translation'; | ||||
import { Paging } from '../../../../types/types'; | |||||
import { ImportProjectParam } from '../CreateProjectPage'; | import { ImportProjectParam } from '../CreateProjectPage'; | ||||
import { REPOSITORY_PAGE_SIZE } from '../constants'; | |||||
import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate'; | import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate'; | ||||
import { CreateProjectModes } from '../types'; | import { CreateProjectModes } from '../types'; | ||||
import { useProjectCreate } from '../useProjectCreate'; | |||||
import { useProjectRepositorySearch } from '../useProjectRepositorySearch'; | |||||
import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm'; | import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm'; | ||||
import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer'; | import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer'; | ||||
interface Props { | interface Props { | ||||
canAdmin: boolean; | |||||
isLoadingBindings: boolean; | isLoadingBindings: boolean; | ||||
onProjectSetupDone: (importProjects: ImportProjectParam) => void; | onProjectSetupDone: (importProjects: ImportProjectParam) => void; | ||||
dopSettings: DopSetting[]; | dopSettings: DopSetting[]; | ||||
} | } | ||||
const REPOSITORY_PAGE_SIZE = 50; | |||||
const REPOSITORY_SEARCH_DEBOUNCE_TIME = 250; | |||||
export default function GitlabProjectCreate(props: Readonly<Props>) { | export default function GitlabProjectCreate(props: Readonly<Props>) { | ||||
const { canAdmin, dopSettings, isLoadingBindings, onProjectSetupDone } = props; | |||||
const repositorySearchDebounceId = useRef<NodeJS.Timeout | undefined>(); | |||||
const [isLoadingRepositories, setIsLoadingRepositories] = useState(false); | |||||
const [repositories, setRepositories] = useState<GitlabProject[]>([]); | |||||
const [repositoryPaging, setRepositoryPaging] = useState<Paging>({ | |||||
pageSize: REPOSITORY_PAGE_SIZE, | |||||
total: 0, | |||||
pageIndex: 1, | |||||
}); | |||||
const [searchQuery, setSearchQuery] = useState(''); | |||||
const [selectedDopSetting, setSelectedDopSetting] = useState<DopSetting>(); | |||||
const [selectedRepository, setSelectedRepository] = useState<GitlabProject>(); | |||||
const [resetPersonalAccessToken, setResetPersonalAccessToken] = useState<boolean>(false); | |||||
const [showPersonalAccessTokenForm, setShowPersonalAccessTokenForm] = useState<boolean>(true); | |||||
const { dopSettings, isLoadingBindings, onProjectSetupDone } = props; | |||||
const { | |||||
handlePersonalAccessTokenCreated, | |||||
handleSelectRepository, | |||||
isInitialized, | |||||
isLoadingRepositories, | |||||
isMonorepoSetup, | |||||
onSelectedAlmInstanceChange, | |||||
onSelectDopSetting, | |||||
projectsPaging, | |||||
repositories, | |||||
resetPersonalAccessToken, | |||||
searchQuery, | |||||
selectedDopSetting, | |||||
selectedRepository, | |||||
setIsInitialized, | |||||
setIsLoadingRepositories, | |||||
setProjectsPaging, | |||||
setRepositories, | |||||
setResetPersonalAccessToken, | |||||
setSearchQuery, | |||||
setShowPersonalAccessTokenForm, | |||||
showPersonalAccessTokenForm, | |||||
} = useProjectCreate<GitlabProject, undefined>( | |||||
AlmKeys.GitLab, | |||||
dopSettings, | |||||
({ id }) => id, | |||||
REPOSITORY_PAGE_SIZE, | |||||
); | |||||
const location = useLocation(); | const location = useLocation(); | ||||
const router = useRouter(); | |||||
const isMonorepoSetup = location.query?.mono === 'true'; | |||||
const hasDopSettings = useMemo(() => { | |||||
if (dopSettings === undefined) { | |||||
return false; | |||||
} | |||||
return dopSettings.length > 0; | |||||
}, [dopSettings]); | |||||
const repositoryOptions = useMemo(() => { | const repositoryOptions = useMemo(() => { | ||||
return repositories.map(transformToOption); | return repositories.map(transformToOption); | ||||
}, [repositories]); | }, [repositories]); | ||||
const fetchProjects = useCallback( | |||||
(pageIndex = 1, query?: string) => { | |||||
if (!selectedDopSetting) { | |||||
return Promise.resolve(undefined); | |||||
const fetchRepositories = useCallback( | |||||
(_orgKey?: string, query = '', pageIndex = 1, more = false) => { | |||||
if (showPersonalAccessTokenForm || !selectedDopSetting) { | |||||
return Promise.resolve(); | |||||
} | } | ||||
setIsLoadingRepositories(true); | |||||
// eslint-disable-next-line local-rules/no-api-imports | // eslint-disable-next-line local-rules/no-api-imports | ||||
return getGitlabProjects({ | return getGitlabProjects({ | ||||
almSetting: selectedDopSetting.key, | almSetting: selectedDopSetting.key, | ||||
page: pageIndex, | page: pageIndex, | ||||
pageSize: REPOSITORY_PAGE_SIZE, | pageSize: REPOSITORY_PAGE_SIZE, | ||||
query, | query, | ||||
}); | |||||
}, | |||||
[selectedDopSetting], | |||||
); | |||||
const fetchInitialData = useCallback(() => { | |||||
if (!showPersonalAccessTokenForm) { | |||||
setIsLoadingRepositories(true); | |||||
fetchProjects() | |||||
}) | |||||
.then((result) => { | .then((result) => { | ||||
if (result?.projects) { | if (result?.projects) { | ||||
setIsLoadingRepositories(false); | |||||
setProjectsPaging(result.projectsPaging); | |||||
setRepositories( | setRepositories( | ||||
isMonorepoSetup | |||||
? orderBy(result.projects, [(res) => res.name.toLowerCase()], ['asc']) | |||||
more && repositories && repositories.length > 0 | |||||
? [...repositories, ...result.projects] | |||||
: result.projects, | : result.projects, | ||||
); | ); | ||||
setRepositoryPaging(result.projectsPaging); | |||||
} else { | |||||
setIsLoadingRepositories(false); | |||||
setIsInitialized(true); | |||||
} | } | ||||
}) | }) | ||||
.finally(() => { | |||||
setIsLoadingRepositories(false); | |||||
}) | |||||
.catch(() => { | .catch(() => { | ||||
setResetPersonalAccessToken(true); | setResetPersonalAccessToken(true); | ||||
setShowPersonalAccessTokenForm(true); | setShowPersonalAccessTokenForm(true); | ||||
setIsLoadingRepositories(false); | |||||
}); | }); | ||||
} | |||||
}, [fetchProjects, isMonorepoSetup, showPersonalAccessTokenForm]); | |||||
const cleanUrl = useCallback(() => { | |||||
delete location.query.resetPat; | |||||
router.replace(location); | |||||
}, [location, router]); | |||||
const handlePersonalAccessTokenCreated = useCallback(() => { | |||||
cleanUrl(); | |||||
setShowPersonalAccessTokenForm(false); | |||||
setResetPersonalAccessToken(false); | |||||
fetchInitialData(); | |||||
}, [cleanUrl, fetchInitialData]); | |||||
}, | |||||
[ | |||||
repositories, | |||||
selectedDopSetting, | |||||
setIsInitialized, | |||||
setIsLoadingRepositories, | |||||
setProjectsPaging, | |||||
setRepositories, | |||||
setResetPersonalAccessToken, | |||||
setShowPersonalAccessTokenForm, | |||||
showPersonalAccessTokenForm, | |||||
], | |||||
); | |||||
const handleImportRepository = useCallback( | const handleImportRepository = useCallback( | ||||
(repoKeys: string[]) => { | (repoKeys: string[]) => { | ||||
[onProjectSetupDone, selectedDopSetting], | [onProjectSetupDone, selectedDopSetting], | ||||
); | ); | ||||
const handleLoadMore = useCallback(async () => { | |||||
const result = await fetchProjects(repositoryPaging.pageIndex + 1, searchQuery); | |||||
if (result?.projects) { | |||||
setRepositoryPaging(result ? result.projectsPaging : repositoryPaging); | |||||
setRepositories(result ? [...repositories, ...result.projects] : repositories); | |||||
} | |||||
}, [fetchProjects, repositories, repositoryPaging, searchQuery]); | |||||
const handleSelectRepository = useCallback( | |||||
(repositoryKey: string) => { | |||||
setSelectedRepository(repositories.find(({ id }) => id === repositoryKey)); | |||||
}, | |||||
[repositories], | |||||
); | |||||
const onSelectDopSetting = useCallback((setting: DopSetting | undefined) => { | |||||
setSelectedDopSetting(setting); | |||||
setShowPersonalAccessTokenForm(true); | |||||
setRepositories([]); | |||||
setSearchQuery(''); | |||||
}, []); | |||||
const onSelectedAlmInstanceChange = useCallback( | |||||
(instance: AlmInstanceBase) => { | |||||
onSelectDopSetting(dopSettings.find((dopSetting) => dopSetting.key === instance.key)); | |||||
}, | |||||
[dopSettings, onSelectDopSetting], | |||||
const handleLoadMore = useCallback(() => { | |||||
fetchRepositories(undefined, searchQuery, projectsPaging.pageIndex + 1, true); | |||||
}, [fetchRepositories, projectsPaging, searchQuery]); | |||||
const { onSearch } = useProjectRepositorySearch( | |||||
AlmKeys.GitLab, | |||||
fetchRepositories, | |||||
isInitialized, | |||||
selectedDopSetting, | |||||
undefined, | |||||
setSearchQuery, | |||||
showPersonalAccessTokenForm, | |||||
); | ); | ||||
useEffect(() => { | |||||
if (dopSettings.length > 0) { | |||||
setSelectedDopSetting(dopSettings[0]); | |||||
return; | |||||
} | |||||
setSelectedDopSetting(undefined); | |||||
// eslint-disable-next-line react-hooks/exhaustive-deps | |||||
}, [hasDopSettings]); | |||||
useEffect(() => { | |||||
if (selectedDopSetting) { | |||||
fetchInitialData(); | |||||
} | |||||
}, [fetchInitialData, selectedDopSetting]); | |||||
useEffect(() => { | |||||
repositorySearchDebounceId.current = setTimeout(async () => { | |||||
const result = await fetchProjects(1, searchQuery); | |||||
if (result?.projects) { | |||||
setRepositories(orderBy(result.projects, [(res) => res.name.toLowerCase()], ['asc'])); | |||||
setRepositoryPaging(result.projectsPaging); | |||||
} | |||||
}, REPOSITORY_SEARCH_DEBOUNCE_TIME); | |||||
return () => { | |||||
clearTimeout(repositorySearchDebounceId.current); | |||||
}; | |||||
// eslint-disable-next-line react-hooks/exhaustive-deps | |||||
}, [searchQuery]); | |||||
return isMonorepoSetup ? ( | return isMonorepoSetup ? ( | ||||
<MonorepoProjectCreate | <MonorepoProjectCreate | ||||
canAdmin={canAdmin} | |||||
dopSettings={dopSettings} | dopSettings={dopSettings} | ||||
error={false} | error={false} | ||||
loadingBindings={isLoadingBindings} | loadingBindings={isLoadingBindings} | ||||
loadingOrganizations={false} | loadingOrganizations={false} | ||||
loadingRepositories={isLoadingRepositories} | loadingRepositories={isLoadingRepositories} | ||||
onProjectSetupDone={onProjectSetupDone} | onProjectSetupDone={onProjectSetupDone} | ||||
onSearchRepositories={setSearchQuery} | |||||
onSearchRepositories={onSearch} | |||||
onSelectDopSetting={onSelectDopSetting} | onSelectDopSetting={onSelectDopSetting} | ||||
onSelectRepository={handleSelectRepository} | onSelectRepository={handleSelectRepository} | ||||
personalAccessTokenComponent={ | personalAccessTokenComponent={ | ||||
key: dopSetting.key, | key: dopSetting.key, | ||||
url: dopSetting.url, | url: dopSetting.url, | ||||
}))} | }))} | ||||
canAdmin={canAdmin} | |||||
loading={isLoadingRepositories || isLoadingBindings} | loading={isLoadingRepositories || isLoadingBindings} | ||||
onImport={handleImportRepository} | onImport={handleImportRepository} | ||||
onLoadMore={handleLoadMore} | onLoadMore={handleLoadMore} | ||||
onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated} | onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated} | ||||
onSearch={setSearchQuery} | |||||
onSearch={onSearch} | |||||
onSelectedAlmInstanceChange={onSelectedAlmInstanceChange} | onSelectedAlmInstanceChange={onSelectedAlmInstanceChange} | ||||
projects={repositories} | projects={repositories} | ||||
projectsPaging={repositoryPaging} | |||||
projectsPaging={projectsPaging} | |||||
resetPat={resetPersonalAccessToken || Boolean(location.query.resetPat)} | resetPat={resetPersonalAccessToken || Boolean(location.query.resetPat)} | ||||
searchQuery={searchQuery} | searchQuery={searchQuery} | ||||
selectedAlmInstance={ | selectedAlmInstance={ |
import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm'; | import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm'; | ||||
export interface GitlabProjectCreateRendererProps { | export interface GitlabProjectCreateRendererProps { | ||||
canAdmin?: boolean; | |||||
almInstances?: AlmSettingsInstance[]; | |||||
loading: boolean; | loading: boolean; | ||||
onImport: (id: string[]) => void; | onImport: (id: string[]) => void; | ||||
onLoadMore: () => void; | onLoadMore: () => void; | ||||
projectsPaging: Paging; | projectsPaging: Paging; | ||||
resetPat: boolean; | resetPat: boolean; | ||||
searchQuery: string; | searchQuery: string; | ||||
almInstances?: AlmSettingsInstance[]; | |||||
selectedAlmInstance?: AlmSettingsInstance; | selectedAlmInstance?: AlmSettingsInstance; | ||||
showPersonalAccessTokenForm?: boolean; | |||||
onSelectedAlmInstanceChange: (instance: AlmInstanceBase) => void; | onSelectedAlmInstanceChange: (instance: AlmInstanceBase) => void; | ||||
showPersonalAccessTokenForm?: boolean; | |||||
} | } | ||||
export default function GitlabProjectCreateRenderer( | export default function GitlabProjectCreateRenderer( | ||||
const { | const { | ||||
almInstances, | almInstances, | ||||
canAdmin, | |||||
loading, | loading, | ||||
onLoadMore, | onLoadMore, | ||||
onSearch, | onSearch, | ||||
<Spinner isLoading={loading} /> | <Spinner isLoading={loading} /> | ||||
{!loading && !selectedAlmInstance && ( | |||||
<WrongBindingCountAlert alm={AlmKeys.GitLab} canAdmin={!!canAdmin} /> | |||||
{!loading && almInstances && almInstances.length === 0 && !selectedAlmInstance && ( | |||||
<WrongBindingCountAlert alm={AlmKeys.GitLab} /> | |||||
)} | )} | ||||
{!loading && | {!loading && |
import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock'; | import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock'; | ||||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | import { renderApp } from '../../../../helpers/testReactTestingUtils'; | ||||
import { byLabelText, byRole, byText } from '../../../../helpers/testSelector'; | import { byLabelText, byRole, byText } from '../../../../helpers/testSelector'; | ||||
import { Feature } from '../../../../types/features'; | |||||
import CreateProjectPage from '../CreateProjectPage'; | import CreateProjectPage from '../CreateProjectPage'; | ||||
import { BITBUCKET_CLOUD_PROJECTS_PAGESIZE } from '../constants'; | |||||
import { REPOSITORY_PAGE_SIZE } from '../constants'; | |||||
import { CreateProjectModes } from '../types'; | |||||
jest.mock('../../../../api/alm-integrations'); | jest.mock('../../../../api/alm-integrations'); | ||||
jest.mock('../../../../api/alm-settings'); | jest.mock('../../../../api/alm-settings'); | ||||
let newCodePeriodHandler: NewCodeDefinitionServiceMock; | let newCodePeriodHandler: NewCodeDefinitionServiceMock; | ||||
const ui = { | const ui = { | ||||
cancelButton: byRole('button', { name: 'cancel' }), | |||||
bitbucketCloudCreateProjectButton: byText( | bitbucketCloudCreateProjectButton: byText( | ||||
'onboarding.create_project.select_method.bitbucketcloud', | 'onboarding.create_project.select_method.bitbucketcloud', | ||||
), | ), | ||||
bitbucketCloudOnboardingTitle: byRole('heading', { | |||||
name: 'onboarding.create_project.bitbucketcloud.title', | |||||
}), | |||||
monorepoSetupLink: byRole('link', { | |||||
name: 'onboarding.create_project.subtitle_monorepo_setup_link', | |||||
}), | |||||
monorepoTitle: byRole('heading', { | |||||
name: 'onboarding.create_project.monorepo.titlealm.bitbucketcloud', | |||||
}), | |||||
personalAccessTokenInput: byRole('textbox', { | personalAccessTokenInput: byRole('textbox', { | ||||
name: /onboarding.create_project.enter_pat/, | name: /onboarding.create_project.enter_pat/, | ||||
}), | }), | ||||
instanceSelector: byLabelText(/alm.configuration.selector.label/), | instanceSelector: byLabelText(/alm.configuration.selector.label/), | ||||
userName: byRole('textbox', { | |||||
name: /onboarding\.create_project\.bitbucket_cloud\.enter_username/, | |||||
}), | |||||
password: byRole('textbox', { | |||||
name: /onboarding\.create_project\.bitbucket_cloud\.enter_password/, | |||||
}), | |||||
}; | }; | ||||
const original = window.location; | const original = window.location; | ||||
expect(screen.getByText('onboarding.create_project.bitbucketcloud.title')).toBeInTheDocument(); | expect(screen.getByText('onboarding.create_project.bitbucketcloud.title')).toBeInTheDocument(); | ||||
expect(await ui.instanceSelector.find()).toBeInTheDocument(); | expect(await ui.instanceSelector.find()).toBeInTheDocument(); | ||||
await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketcloud-1/]); | |||||
expect( | expect( | ||||
screen.getByText('onboarding.create_project.bitbucket_cloud.enter_password'), | |||||
await screen.findByText('onboarding.create_project.bitbucket_cloud.enter_password'), | |||||
).toBeInTheDocument(); | ).toBeInTheDocument(); | ||||
expect( | expect( | ||||
screen.getByText('onboarding.create_project.enter_password.instructions.bitbucket_cloud'), | screen.getByText('onboarding.create_project.enter_password.instructions.bitbucket_cloud'), | ||||
expect(screen.getByRole('button', { name: 'save' })).toBeDisabled(); | expect(screen.getByRole('button', { name: 'save' })).toBeDisabled(); | ||||
await user.click( | |||||
screen.getByRole('textbox', { | |||||
name: /onboarding.create_project.bitbucket_cloud.enter_username/, | |||||
}), | |||||
); | |||||
await user.click(ui.userName.get()); | |||||
await user.type(ui.userName.get(), 'username'); | |||||
await user.keyboard('username'); | |||||
expect(ui.userName.get()).toHaveValue('username'); | |||||
await user.click( | |||||
screen.getByRole('textbox', { | |||||
name: /onboarding.create_project.bitbucket_cloud.enter_password/, | |||||
}), | |||||
); | |||||
await user.click(ui.password.get()); | |||||
await user.type(ui.password.get(), 'password'); | |||||
await user.keyboard('password'); | |||||
expect(ui.password.get()).toHaveValue('password'); | |||||
expect(screen.getByRole('button', { name: 'save' })).toBeEnabled(); | expect(screen.getByRole('button', { name: 'save' })).toBeEnabled(); | ||||
await user.click(screen.getByRole('button', { name: 'save' })); | await user.click(screen.getByRole('button', { name: 'save' })); | ||||
expect(searchForBitbucketCloudRepositories).toHaveBeenLastCalledWith( | expect(searchForBitbucketCloudRepositories).toHaveBeenLastCalledWith( | ||||
'conf-bitbucketcloud-2', | 'conf-bitbucketcloud-2', | ||||
'', | '', | ||||
BITBUCKET_CLOUD_PROJECTS_PAGESIZE, | |||||
REPOSITORY_PAGE_SIZE, | |||||
1, | 1, | ||||
), | ), | ||||
); | ); | ||||
name: 'onboarding.create_project.search_prompt', | name: 'onboarding.create_project.search_prompt', | ||||
}); | }); | ||||
await user.click(inputSearch); | await user.click(inputSearch); | ||||
await user.keyboard('search'); | |||||
await user.type(inputSearch, 'search'); | |||||
expect(searchForBitbucketCloudRepositories).toHaveBeenLastCalledWith( | |||||
'conf-bitbucketcloud-2', | |||||
'search', | |||||
BITBUCKET_CLOUD_PROJECTS_PAGESIZE, | |||||
1, | |||||
await waitFor(() => | |||||
expect(searchForBitbucketCloudRepositories).toHaveBeenLastCalledWith( | |||||
'conf-bitbucketcloud-2', | |||||
'search', | |||||
REPOSITORY_PAGE_SIZE, | |||||
1, | |||||
), | |||||
); | ); | ||||
}); | }); | ||||
it('should have load more', async () => { | it('should have load more', async () => { | ||||
const user = userEvent.setup(); | const user = userEvent.setup(); | ||||
almIntegrationHandler.createRandomBitbucketCloudProjectsWithLoadMore( | almIntegrationHandler.createRandomBitbucketCloudProjectsWithLoadMore( | ||||
BITBUCKET_CLOUD_PROJECTS_PAGESIZE, | |||||
BITBUCKET_CLOUD_PROJECTS_PAGESIZE + 1, | |||||
REPOSITORY_PAGE_SIZE, | |||||
REPOSITORY_PAGE_SIZE + 1, | |||||
); | ); | ||||
renderCreateProject(); | renderCreateProject(); | ||||
* loadmore button disapperance. | * loadmore button disapperance. | ||||
*/ | */ | ||||
almIntegrationHandler.createRandomBitbucketCloudProjectsWithLoadMore( | almIntegrationHandler.createRandomBitbucketCloudProjectsWithLoadMore( | ||||
BITBUCKET_CLOUD_PROJECTS_PAGESIZE + 1, | |||||
BITBUCKET_CLOUD_PROJECTS_PAGESIZE + 1, | |||||
REPOSITORY_PAGE_SIZE + 1, | |||||
REPOSITORY_PAGE_SIZE + 1, | |||||
); | ); | ||||
await user.click(screen.getByRole('button', { name: 'show_more' })); | await user.click(screen.getByRole('button', { name: 'show_more' })); | ||||
expect(searchForBitbucketCloudRepositories).toHaveBeenLastCalledWith( | |||||
'conf-bitbucketcloud-2', | |||||
'', | |||||
BITBUCKET_CLOUD_PROJECTS_PAGESIZE, | |||||
2, | |||||
await waitFor(() => | |||||
expect(searchForBitbucketCloudRepositories).toHaveBeenLastCalledWith( | |||||
'conf-bitbucketcloud-2', | |||||
'', | |||||
REPOSITORY_PAGE_SIZE, | |||||
2, | |||||
), | |||||
); | ); | ||||
await waitFor(() => { | await waitFor(() => { | ||||
}); | }); | ||||
}); | }); | ||||
function renderCreateProject() { | |||||
renderApp('project/create', <CreateProjectPage />, { | |||||
navigateTo: 'project/create?mode=bitbucketcloud', | |||||
describe('Bitbucket Cloud monorepo project navigation', () => { | |||||
it('should be able to access monorepo setup page from Bitbucket Cloud project import page', async () => { | |||||
const user = userEvent.setup(); | |||||
renderCreateProject(); | |||||
await user.click(await ui.monorepoSetupLink.find()); | |||||
expect(ui.monorepoTitle.get()).toBeInTheDocument(); | |||||
}); | |||||
it('should be able to go back to Bitbucket Cloud onboarding page from monorepo setup page', async () => { | |||||
const user = userEvent.setup(); | |||||
renderCreateProject({ isMonorepo: true }); | |||||
await user.click(await ui.cancelButton.find()); | |||||
expect(ui.bitbucketCloudOnboardingTitle.get()).toBeInTheDocument(); | |||||
}); | |||||
}); | |||||
function renderCreateProject({ | |||||
isMonorepo = false, | |||||
}: { | |||||
isMonorepo?: boolean; | |||||
} = {}) { | |||||
let queryString = `mode=${CreateProjectModes.BitbucketCloud}`; | |||||
if (isMonorepo) { | |||||
queryString += '&mono=true'; | |||||
} | |||||
renderApp('projects/create', <CreateProjectPage />, { | |||||
navigateTo: `projects/create?${queryString}`, | |||||
featureList: [Feature.MonoRepositoryPullRequestDecoration], | |||||
}); | }); | ||||
} | } |
expect(await ui.importProjectsTitle.find()).toBeInTheDocument(); | expect(await ui.importProjectsTitle.find()).toBeInTheDocument(); | ||||
expect(ui.instanceSelector.get()).toBeInTheDocument(); | expect(ui.instanceSelector.get()).toBeInTheDocument(); | ||||
await selectEvent.select(ui.instanceSelector.get(), [/conf-final-1/]); | |||||
expect(screen.getByText('onboarding.create_project.enter_pat')).toBeInTheDocument(); | |||||
expect(await screen.findByText('onboarding.create_project.enter_pat')).toBeInTheDocument(); | |||||
expect(ui.patHelpInstructions.get()).toBeInTheDocument(); | expect(ui.patHelpInstructions.get()).toBeInTheDocument(); | ||||
expect(ui.saveButton.get()).toBeInTheDocument(); | expect(ui.saveButton.get()).toBeInTheDocument(); | ||||
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { FlagMessage, Link } from 'design-system'; | |||||
import { Link } from '@sonarsource/echoes-react'; | |||||
import { FlagMessage } from 'design-system'; | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||
import { useAppState } from '../../../../app/components/app-state/withAppStateContext'; | |||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { getGlobalSettingsUrl } from '../../../../helpers/urls'; | import { getGlobalSettingsUrl } from '../../../../helpers/urls'; | ||||
import { AlmKeys } from '../../../../types/alm-settings'; | import { AlmKeys } from '../../../../types/alm-settings'; | ||||
export interface WrongBindingCountAlertProps { | export interface WrongBindingCountAlertProps { | ||||
alm: AlmKeys; | alm: AlmKeys; | ||||
canAdmin: boolean; | |||||
} | } | ||||
export default function WrongBindingCountAlert(props: WrongBindingCountAlertProps) { | export default function WrongBindingCountAlert(props: WrongBindingCountAlertProps) { | ||||
const { alm, canAdmin } = props; | |||||
const { alm } = props; | |||||
const { canAdmin } = useAppState(); | |||||
return ( | return ( | ||||
<FlagMessage variant="error" className="sw-mb-2"> | <FlagMessage variant="error" className="sw-mb-2"> |
export const DEFAULT_BBS_PAGE_SIZE = 25; | export const DEFAULT_BBS_PAGE_SIZE = 25; | ||||
export const BITBUCKET_CLOUD_PROJECTS_PAGESIZE = 20; | |||||
export const REPOSITORY_PAGE_SIZE = 50; | |||||
export const REPOSITORY_SEARCH_DEBOUNCE_TIME = 250; |
projectId: string; | projectId: string; | ||||
projectName: string; | projectName: string; | ||||
}[]; | }[]; | ||||
canAdmin: boolean; | |||||
dopSettings: DopSetting[]; | dopSettings: DopSetting[]; | ||||
error: boolean; | error: boolean; | ||||
isFetchingAlreadyBoundProjects: boolean; | isFetchingAlreadyBoundProjects: boolean; | ||||
export function MonorepoConnectionSelector({ | export function MonorepoConnectionSelector({ | ||||
almKey, | almKey, | ||||
alreadyBoundProjects, | alreadyBoundProjects, | ||||
canAdmin, | |||||
dopSettings, | dopSettings, | ||||
error, | error, | ||||
isFetchingAlreadyBoundProjects, | isFetchingAlreadyBoundProjects, | ||||
) : ( | ) : ( | ||||
<> | <> | ||||
{showOrganizations && error && selectedDopSetting && !loadingOrganizations && ( | {showOrganizations && error && selectedDopSetting && !loadingOrganizations && ( | ||||
<MonorepoNoOrganisations almKey={almKey} canAdmin={canAdmin} /> | |||||
<MonorepoNoOrganisations almKey={almKey} /> | |||||
)} | )} | ||||
{showOrganizations && organizationOptions && ( | {showOrganizations && organizationOptions && ( | ||||
<div className="sw-flex sw-flex-col"> | <div className="sw-flex sw-flex-col"> | ||||
<MonorepoOrganisationSelector | <MonorepoOrganisationSelector | ||||
almKey={almKey} | almKey={almKey} | ||||
canAdmin={canAdmin} | |||||
error={error} | error={error} | ||||
organizationOptions={organizationOptions} | organizationOptions={organizationOptions} | ||||
loadingOrganizations={loadingOrganizations} | loadingOrganizations={loadingOrganizations} |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { Link } from '@sonarsource/echoes-react'; | import { Link } from '@sonarsource/echoes-react'; | ||||
import { FlagMessage } from 'design-system/lib'; | |||||
import { FlagMessage } from 'design-system'; | |||||
import React from 'react'; | import React from 'react'; | ||||
import { FormattedMessage, useIntl } from 'react-intl'; | import { FormattedMessage, useIntl } from 'react-intl'; | ||||
import { useAppState } from '../../../../app/components/app-state/withAppStateContext'; | |||||
import { AlmKeys } from '../../../../types/alm-settings'; | import { AlmKeys } from '../../../../types/alm-settings'; | ||||
export default function MonorepoNoOrganisations({ | |||||
almKey, | |||||
canAdmin, | |||||
}: Readonly<{ almKey: AlmKeys; canAdmin: boolean }>) { | |||||
export default function MonorepoNoOrganisations({ almKey }: Readonly<{ almKey: AlmKeys }>) { | |||||
const { formatMessage } = useIntl(); | const { formatMessage } = useIntl(); | ||||
const { canAdmin } = useAppState(); | |||||
return ( | return ( | ||||
<FlagMessage variant="warning"> | <FlagMessage variant="warning"> |
import { DarkLabel, FlagMessage, InputSelect } from 'design-system'; | import { DarkLabel, FlagMessage, InputSelect } from 'design-system'; | ||||
import React from 'react'; | import React from 'react'; | ||||
import { FormattedMessage, useIntl } from 'react-intl'; | import { FormattedMessage, useIntl } from 'react-intl'; | ||||
import { useAppState } from '../../../../app/components/app-state/withAppStateContext'; | |||||
import { LabelValueSelectOption } from '../../../../helpers/search'; | import { LabelValueSelectOption } from '../../../../helpers/search'; | ||||
import { AlmKeys } from '../../../../types/alm-settings'; | import { AlmKeys } from '../../../../types/alm-settings'; | ||||
interface Props { | interface Props { | ||||
almKey: AlmKeys; | almKey: AlmKeys; | ||||
canAdmin: boolean; | |||||
error: boolean; | error: boolean; | ||||
loadingOrganizations?: boolean; | loadingOrganizations?: boolean; | ||||
onSelectOrganization?: (organizationKey: string) => void; | onSelectOrganization?: (organizationKey: string) => void; | ||||
export function MonorepoOrganisationSelector({ | export function MonorepoOrganisationSelector({ | ||||
almKey, | almKey, | ||||
canAdmin, | |||||
error, | error, | ||||
loadingOrganizations, | loadingOrganizations, | ||||
onSelectOrganization, | onSelectOrganization, | ||||
selectedOrganization, | selectedOrganization, | ||||
}: Readonly<Props>) { | }: Readonly<Props>) { | ||||
const { formatMessage } = useIntl(); | const { formatMessage } = useIntl(); | ||||
const { canAdmin } = useAppState(); | |||||
return ( | return ( | ||||
!error && ( | !error && ( | ||||
<Spinner isLoading={loadingOrganizations && !error}> | <Spinner isLoading={loadingOrganizations && !error}> | ||||
{organizationOptions.length > 0 ? ( | {organizationOptions.length > 0 ? ( | ||||
<InputSelect | <InputSelect | ||||
size="large" | |||||
size="full" | |||||
isSearchable | isSearchable | ||||
inputId={`${almKey}-monorepo-choose-organization`} | inputId={`${almKey}-monorepo-choose-organization`} | ||||
options={organizationOptions} | options={organizationOptions} |
import { MonorepoProjectsList } from './MonorepoProjectsList'; | import { MonorepoProjectsList } from './MonorepoProjectsList'; | ||||
interface MonorepoProjectCreateProps { | interface MonorepoProjectCreateProps { | ||||
canAdmin: boolean; | |||||
dopSettings: DopSetting[]; | dopSettings: DopSetting[]; | ||||
error: boolean; | error: boolean; | ||||
loadingBindings: boolean; | loadingBindings: boolean; |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { LinkHighlight, LinkStandalone, Spinner } from '@sonarsource/echoes-react'; | |||||
import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react'; | |||||
import { DarkLabel, FlagMessage, InputSelect } from 'design-system'; | import { DarkLabel, FlagMessage, InputSelect } from 'design-system'; | ||||
import React from 'react'; | import React from 'react'; | ||||
import { FormattedMessage, useIntl } from 'react-intl'; | import { FormattedMessage, useIntl } from 'react-intl'; | ||||
!loadingRepositories && | !loadingRepositories && | ||||
((showOrganizations && !!selectedOrganization) || !showOrganizations); | ((showOrganizations && !!selectedOrganization) || !showOrganizations); | ||||
const showWarningMessage = | const showWarningMessage = | ||||
error || (repositorySelectorEnabled && repositoryOptions && repositoryOptions.length === 0); | |||||
error || | |||||
(repositorySelectorEnabled && | |||||
repositoryOptions && | |||||
repositoryOptions.length === 0 && | |||||
repositorySearchQuery === ''); | |||||
return ( | return ( | ||||
<> | <> | ||||
<DarkLabel htmlFor={`${almKey}-monorepo-choose-repository`} className="sw-mb-2"> | <DarkLabel htmlFor={`${almKey}-monorepo-choose-repository`} className="sw-mb-2"> | ||||
<FormattedMessage id="onboarding.create_project.monorepo.choose_repository" /> | <FormattedMessage id="onboarding.create_project.monorepo.choose_repository" /> | ||||
</DarkLabel> | </DarkLabel> | ||||
<Spinner isLoading={loadingRepositories && !error}> | |||||
{showWarningMessage ? ( | |||||
<FormattedMessage | |||||
id="onboarding.create_project.monorepo.no_projects" | |||||
defaultMessage={formatMessage({ id: 'onboarding.create_project.monorepo.no_projects' })} | |||||
values={{ | |||||
almKey: formatMessage({ id: `alm.${almKey}` }), | |||||
{showWarningMessage ? ( | |||||
<FormattedMessage | |||||
id="onboarding.create_project.monorepo.no_projects" | |||||
defaultMessage={formatMessage({ id: 'onboarding.create_project.monorepo.no_projects' })} | |||||
values={{ | |||||
almKey: formatMessage({ id: `alm.${almKey}` }), | |||||
}} | |||||
/> | |||||
) : ( | |||||
<> | |||||
<InputSelect | |||||
inputId={`${almKey}-monorepo-choose-repository`} | |||||
inputValue={repositorySearchQuery} | |||||
isLoading={loadingRepositories} | |||||
isSearchable | |||||
noOptionsMessage={() => formatMessage({ id: 'no_results' })} | |||||
onChange={({ value }: LabelValueSelectOption) => { | |||||
onSelectRepository(value); | |||||
}} | }} | ||||
onInputChange={onSearchRepositories} | |||||
options={repositoryOptions} | |||||
placeholder={formatMessage({ | |||||
id: `onboarding.create_project.monorepo.choose_repository.placeholder`, | |||||
})} | |||||
size="full" | |||||
value={selectedRepository} | |||||
/> | /> | ||||
) : ( | |||||
<> | |||||
<InputSelect | |||||
inputId={`${almKey}-monorepo-choose-repository`} | |||||
inputValue={repositorySearchQuery} | |||||
isDisabled={!repositorySelectorEnabled} | |||||
isLoading={loadingRepositories} | |||||
isSearchable | |||||
noOptionsMessage={() => formatMessage({ id: 'no_results' })} | |||||
onChange={({ value }: LabelValueSelectOption) => { | |||||
onSelectRepository(value); | |||||
}} | |||||
onInputChange={onSearchRepositories} | |||||
options={repositoryOptions} | |||||
placeholder={formatMessage({ | |||||
id: `onboarding.create_project.monorepo.choose_repository.placeholder`, | |||||
})} | |||||
size="full" | |||||
value={selectedRepository} | |||||
/> | |||||
{selectedRepository && | |||||
!isLoadingAlreadyBoundProjects && | |||||
!isFetchingAlreadyBoundProjects && ( | |||||
<FlagMessage className="sw-mt-2" variant="info"> | |||||
{alreadyBoundProjects.length === 0 ? ( | |||||
<FormattedMessage id="onboarding.create_project.monorepo.choose_repository.no_already_bound_projects" /> | |||||
) : ( | |||||
<div> | |||||
<FormattedMessage id="onboarding.create_project.monorepo.choose_repository.existing_already_bound_projects" /> | |||||
<ul className="sw-mt-4"> | |||||
{alreadyBoundProjects.map(({ projectId, projectName }) => ( | |||||
<li key={projectId}> | |||||
<LinkStandalone | |||||
to={getProjectUrl(projectId)} | |||||
highlight={LinkHighlight.Subdued} | |||||
> | |||||
{projectName} | |||||
</LinkStandalone> | |||||
</li> | |||||
))} | |||||
</ul> | |||||
</div> | |||||
)} | |||||
</FlagMessage> | |||||
)} | |||||
</> | |||||
)} | |||||
</Spinner> | |||||
{selectedRepository && | |||||
!isLoadingAlreadyBoundProjects && | |||||
!isFetchingAlreadyBoundProjects && ( | |||||
<FlagMessage className="sw-mt-2" variant="info"> | |||||
{alreadyBoundProjects.length === 0 ? ( | |||||
<FormattedMessage id="onboarding.create_project.monorepo.choose_repository.no_already_bound_projects" /> | |||||
) : ( | |||||
<div> | |||||
<FormattedMessage id="onboarding.create_project.monorepo.choose_repository.existing_already_bound_projects" /> | |||||
<ul className="sw-mt-4"> | |||||
{alreadyBoundProjects.map(({ projectId, projectName }) => ( | |||||
<li key={projectId}> | |||||
<LinkStandalone | |||||
to={getProjectUrl(projectId)} | |||||
highlight={LinkHighlight.Subdued} | |||||
> | |||||
{projectName} | |||||
</LinkStandalone> | |||||
</li> | |||||
))} | |||||
</ul> | |||||
</div> | |||||
)} | |||||
</FlagMessage> | |||||
)} | |||||
</> | |||||
)} | |||||
</> | </> | ||||
); | ); | ||||
} | } |
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2024 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 { useCallback, useEffect, useMemo, useState } from 'react'; | |||||
import { useLocation, useRouter } from '../../../components/hoc/withRouter'; | |||||
import { isDefined } from '../../../helpers/types'; | |||||
import { AlmInstanceBase, AlmKeys } from '../../../types/alm-settings'; | |||||
import { DopSetting } from '../../../types/dop-translation'; | |||||
import { Paging } from '../../../types/types'; | |||||
export function useProjectCreate<RepoType, GroupType>( | |||||
almKey: AlmKeys, | |||||
dopSettings: DopSetting[], | |||||
getKey: (repo: RepoType) => string, | |||||
pageSize: number, | |||||
) { | |||||
const [isInitialized, setIsInitialized] = useState(false); | |||||
const [selectedDopSetting, setSelectedDopSetting] = useState<DopSetting>(); | |||||
const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(true); | |||||
const [organizations, setOrganizations] = useState<GroupType[]>([]); | |||||
const [selectedOrganization, setSelectedOrganization] = useState<GroupType>(); | |||||
const [isLoadingRepositories, setIsLoadingRepositories] = useState<boolean>(false); | |||||
const [isLoadingMoreRepositories, setIsLoadingMoreRepositories] = useState<boolean>(false); | |||||
const [repositories, setRepositories] = useState<RepoType[]>([]); | |||||
const [selectedRepository, setSelectedRepository] = useState<RepoType>(); | |||||
const [showPersonalAccessTokenForm, setShowPersonalAccessTokenForm] = useState<boolean>(true); | |||||
const [resetPersonalAccessToken, setResetPersonalAccessToken] = useState<boolean>(false); | |||||
const [searchQuery, setSearchQuery] = useState<string>(''); | |||||
const [projectsPaging, setProjectsPaging] = useState<Paging>({ | |||||
pageIndex: 1, | |||||
pageSize, | |||||
total: 0, | |||||
}); | |||||
const router = useRouter(); | |||||
const location = useLocation(); | |||||
const isMonorepoSetup = location.query?.mono === 'true'; | |||||
const hasDopSettings = useMemo(() => Boolean(dopSettings?.length), [dopSettings]); | |||||
const cleanUrl = useCallback(() => { | |||||
delete location.query.resetPat; | |||||
router.replace(location); | |||||
}, [location, router]); | |||||
const handlePersonalAccessTokenCreated = useCallback(() => { | |||||
cleanUrl(); | |||||
setShowPersonalAccessTokenForm(false); | |||||
setResetPersonalAccessToken(false); | |||||
}, [cleanUrl]); | |||||
const onSelectDopSetting = useCallback((setting: DopSetting | undefined) => { | |||||
setIsInitialized(false); | |||||
setSelectedDopSetting(setting); | |||||
setShowPersonalAccessTokenForm(true); | |||||
setOrganizations([]); | |||||
setRepositories([]); | |||||
setSearchQuery(''); | |||||
}, []); | |||||
const resetLoading = useCallback((value: boolean, more = false) => { | |||||
if (more) { | |||||
setIsLoadingMoreRepositories(value); | |||||
} else { | |||||
setIsLoadingRepositories(value); | |||||
} | |||||
}, []); | |||||
const onSelectedAlmInstanceChange = useCallback( | |||||
(instance?: AlmInstanceBase) => { | |||||
onSelectDopSetting( | |||||
instance ? dopSettings.find((dopSetting) => dopSetting.key === instance.key) : undefined, | |||||
); | |||||
}, | |||||
[dopSettings, onSelectDopSetting], | |||||
); | |||||
const handleSelectRepository = useCallback( | |||||
(repositoryKey: string) => { | |||||
setSelectedRepository(repositories.find((repo) => getKey(repo) === repositoryKey)); | |||||
}, | |||||
[getKey, repositories, setSelectedRepository], | |||||
); | |||||
useEffect(() => { | |||||
if (!hasDopSettings || (hasDopSettings && isDefined(selectedDopSetting))) { | |||||
return; | |||||
} | |||||
if (almKey === AlmKeys.GitHub) { | |||||
const selectedDopSettingId = location.query?.dopSetting; | |||||
if (selectedDopSettingId !== undefined) { | |||||
const selectedDopSetting = dopSettings.find(({ id }) => id === selectedDopSettingId); | |||||
if (selectedDopSetting) { | |||||
setSelectedDopSetting(selectedDopSetting); | |||||
} | |||||
return; | |||||
} | |||||
} | |||||
if (dopSettings.length > 1) { | |||||
setSelectedDopSetting(undefined); | |||||
} else { | |||||
setSelectedDopSetting(dopSettings[0]); | |||||
} | |||||
}, [almKey, dopSettings, hasDopSettings, location, selectedDopSetting, setSelectedDopSetting]); | |||||
return { | |||||
handlePersonalAccessTokenCreated, | |||||
handleSelectRepository, | |||||
hasDopSettings, | |||||
isInitialized, | |||||
isLoadingOrganizations, | |||||
isLoadingRepositories, | |||||
isLoadingMoreRepositories, | |||||
isMonorepoSetup, | |||||
onSelectedAlmInstanceChange, | |||||
onSelectDopSetting, | |||||
projectsPaging, | |||||
organizations, | |||||
repositories, | |||||
resetLoading, | |||||
resetPersonalAccessToken, | |||||
searchQuery, | |||||
selectedDopSetting, | |||||
selectedRepository, | |||||
setIsInitialized, | |||||
setIsLoadingRepositories, | |||||
setIsLoadingMoreRepositories, | |||||
setIsLoadingOrganizations, | |||||
setProjectsPaging, | |||||
setOrganizations, | |||||
selectedOrganization, | |||||
setRepositories, | |||||
setResetPersonalAccessToken, | |||||
setSearchQuery, | |||||
setSelectedDopSetting, | |||||
setSelectedOrganization, | |||||
setSelectedRepository, | |||||
setShowPersonalAccessTokenForm, | |||||
showPersonalAccessTokenForm, | |||||
}; | |||||
} |
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2024 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||||
import { AlmKeys } from '../../../types/alm-settings'; | |||||
import { DopSetting } from '../../../types/dop-translation'; | |||||
import { REPOSITORY_SEARCH_DEBOUNCE_TIME } from './constants'; | |||||
export function useProjectRepositorySearch( | |||||
almKey: AlmKeys, | |||||
fetchRepositories: ( | |||||
organizationKey?: string, | |||||
query?: string, | |||||
pageIndex?: number, | |||||
more?: boolean, | |||||
) => Promise<void>, | |||||
isInitialized: boolean, | |||||
selectedDopSetting: DopSetting | undefined, | |||||
selectedOrganizationKey: string | undefined, | |||||
setSearchQuery: (query: string) => void, | |||||
showPersonalAccessTokenForm = false, | |||||
) { | |||||
const repositorySearchDebounceId = useRef<NodeJS.Timeout | undefined>(); | |||||
const [isSearching, setIsSearching] = useState<boolean>(false); | |||||
const orgValid = useMemo( | |||||
() => | |||||
almKey !== AlmKeys.GitHub || | |||||
(almKey === AlmKeys.GitHub && selectedOrganizationKey !== undefined), | |||||
[almKey, selectedOrganizationKey], | |||||
); | |||||
useEffect(() => { | |||||
if (selectedDopSetting && !showPersonalAccessTokenForm && orgValid) { | |||||
if (almKey === AlmKeys.GitHub) { | |||||
fetchRepositories(selectedOrganizationKey); | |||||
} else if (!isInitialized) { | |||||
fetchRepositories(); | |||||
} | |||||
} | |||||
}, [ | |||||
almKey, | |||||
fetchRepositories, | |||||
isInitialized, | |||||
orgValid, | |||||
selectedDopSetting, | |||||
selectedOrganizationKey, | |||||
showPersonalAccessTokenForm, | |||||
]); | |||||
const onSearch = useCallback( | |||||
(query: string) => { | |||||
setSearchQuery(query); | |||||
if (!isInitialized || !orgValid) { | |||||
return; | |||||
} | |||||
clearTimeout(repositorySearchDebounceId.current); | |||||
repositorySearchDebounceId.current = setTimeout(() => { | |||||
setIsSearching(true); | |||||
fetchRepositories( | |||||
almKey === AlmKeys.GitHub ? selectedOrganizationKey : undefined, | |||||
query, | |||||
).then( | |||||
() => setIsSearching(false), | |||||
() => setIsSearching(false), | |||||
); | |||||
}, REPOSITORY_SEARCH_DEBOUNCE_TIME); | |||||
}, | |||||
[ | |||||
almKey, | |||||
fetchRepositories, | |||||
isInitialized, | |||||
orgValid, | |||||
repositorySearchDebounceId, | |||||
selectedOrganizationKey, | |||||
setIsSearching, | |||||
setSearchQuery, | |||||
], | |||||
); | |||||
return { | |||||
isSearching, | |||||
onSearch, | |||||
}; | |||||
} |
onboarding.create_project.azure.no_results=No repositories match your search query. | onboarding.create_project.azure.no_results=No repositories match your search query. | ||||
onboarding.create_project.bitbucketcloud.title=Bitbucket Cloud project onboarding | onboarding.create_project.bitbucketcloud.title=Bitbucket Cloud project onboarding | ||||
onboarding.create_project.bitbucketcloud.subtitle=Import projects from one of your Bitbucket Cloud workspaces | onboarding.create_project.bitbucketcloud.subtitle=Import projects from one of your Bitbucket Cloud workspaces | ||||
onboarding.create_project.bitbucketcloud.subtitle.with_monorepo=Import projects from one of your Bitbucket Cloud workspaces or {monorepoSetupLink}. | |||||
onboarding.create_project.bitbucketcloud.no_projects=No projects could be fetched from Bitbucket. Contact your system administrator, or {link}. | onboarding.create_project.bitbucketcloud.no_projects=No projects could be fetched from Bitbucket. Contact your system administrator, or {link}. | ||||
onboarding.create_project.bitbucketcloud.link=See on Bitbucket | onboarding.create_project.bitbucketcloud.link=See on Bitbucket | ||||
onboarding.create_project.github.title=GitHub project onboarding | onboarding.create_project.github.title=GitHub project onboarding |