aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx211
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreateRenderer.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectsList.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketImportRepositoryForm.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreate.tsx415
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreateRenderer.tsx67
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketRepositories.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketServerPersonalAccessTokenForm.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx61
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/useProjectCreate.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/useProjectRepositorySearch.tsx124
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/useRepositorySearch.tsx102
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties1
20 files changed, 682 insertions, 534 deletions
diff --git a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx
index 576e5bd092e..7ef68d6dea5 100644
--- a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx
@@ -18,21 +18,23 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { LabelValueSelectOption } from 'design-system';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import { GroupBase } from 'react-select';
-import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
+import { useLocation } from '~sonar-aligned/components/hoc/withRouter';
import {
getAzureProjects,
getAzureRepositories,
searchAzureRepositories,
} from '../../../../api/alm-integrations';
import { AzureProject, AzureRepository } from '../../../../types/alm-integration';
-import { AlmSettingsInstance } from '../../../../types/alm-settings';
+import { AlmKeys } from '../../../../types/alm-settings';
import { DopSetting } from '../../../../types/dop-translation';
import { Dict } from '../../../../types/types';
import { ImportProjectParam } from '../CreateProjectPage';
import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
import { CreateProjectModes } from '../types';
+import { useProjectCreate } from '../useProjectCreate';
+import { useProjectRepositorySearch } from '../useProjectRepositorySearch';
import AzurePersonalAccessTokenForm from './AzurePersonalAccessTokenForm';
import AzureCreateProjectRenderer from './AzureProjectCreateRenderer';
@@ -42,51 +44,41 @@ interface Props {
onProjectSetupDone: (importProjects: ImportProjectParam) => void;
}
-export default function AzureProjectCreate(props: Readonly<Props>) {
- const { dopSettings, isLoadingBindings, onProjectSetupDone } = props;
- const [isLoading, setIsLoading] = useState(false);
+export default function AzureProjectCreate({
+ dopSettings,
+ isLoadingBindings,
+ onProjectSetupDone,
+}: Readonly<Props>) {
+ const {
+ almInstances,
+ handlePersonalAccessTokenCreated,
+ handleSelectRepository: defaultRepositorySelect,
+ isLoadingRepositories,
+ isMonorepoSetup,
+ onSelectedAlmInstanceChange,
+ organizations: projects,
+ repositories,
+ searchQuery,
+ selectedAlmInstance,
+ selectedDopSetting,
+ selectedRepository,
+ setSearchQuery,
+ setIsLoadingRepositories,
+ setOrganizations: setProjects,
+ setRepositories,
+ setSelectedDopSetting,
+ setSelectedRepository,
+ setShowPersonalAccessTokenForm,
+ showPersonalAccessTokenForm,
+ } = useProjectCreate<AzureRepository, Dict<AzureRepository[]>, AzureProject>(
+ AlmKeys.Azure,
+ dopSettings,
+ ({ name }) => name,
+ );
+
const [loadingRepositories, setLoadingRepositories] = useState<Dict<boolean>>({});
- const [isSearching, setIsSearching] = useState(false);
- const [projects, setProjects] = useState<AzureProject[] | undefined>();
- const [repositories, setRepositories] = useState<Dict<AzureRepository[]>>({});
- const [searchQuery, setSearchQuery] = useState<string>('');
- const [searchResults, setSearchResults] = useState<AzureRepository[] | undefined>();
- const [selectedDopSetting, setSelectedDopSetting] = useState<DopSetting | undefined>();
- const [selectedRepository, setSelectedRepository] = useState<AzureRepository>();
- const [showPersonalAccessTokenForm, setShowPersonalAccessTokenForm] = useState(true);
const location = useLocation();
- const router = useRouter();
-
- const almInstances = useMemo(
- () =>
- dopSettings?.map((dopSetting) => ({
- alm: dopSetting.type,
- key: dopSetting.key,
- url: dopSetting.url,
- })) ?? [],
- [dopSettings],
- );
- const isMonorepoSetup = location.query?.mono === 'true';
- const hasDopSettings = Boolean(dopSettings?.length);
- const selectedAlmInstance = useMemo(
- () =>
- selectedDopSetting && {
- alm: selectedDopSetting.type,
- key: selectedDopSetting.key,
- url: selectedDopSetting.url,
- },
- [selectedDopSetting],
- );
- const repositoryOptions = useMemo(
- () => transformToOptions(projects ?? [], repositories),
- [projects, repositories],
- );
-
- const cleanUrl = useCallback(() => {
- delete location.query.resetPat;
- router.replace(location);
- }, [location, router]);
const fetchAzureProjects = useCallback(async (): Promise<AzureProject[] | undefined> => {
if (selectedDopSetting === undefined) {
@@ -119,13 +111,13 @@ export default function AzureProjectCreate(props: Readonly<Props>) {
return;
}
- setIsLoading(true);
+ setIsLoadingRepositories(true);
let projects: AzureProject[] | undefined;
try {
projects = await fetchAzureProjects();
} catch (_) {
setShowPersonalAccessTokenForm(true);
- setIsLoading(false);
+ setIsLoadingRepositories(false);
return;
}
@@ -171,8 +163,17 @@ export default function AzureProjectCreate(props: Readonly<Props>) {
}
setProjects(projects);
- setIsLoading(false);
- }, [fetchAzureProjects, fetchAzureRepositories, isMonorepoSetup, showPersonalAccessTokenForm]);
+ setIsLoadingRepositories(false);
+ }, [
+ fetchAzureProjects,
+ fetchAzureRepositories,
+ isMonorepoSetup,
+ setIsLoadingRepositories,
+ setProjects,
+ setRepositories,
+ setShowPersonalAccessTokenForm,
+ showPersonalAccessTokenForm,
+ ]);
const handleImportRepository = useCallback(
(selectedRepository: AzureRepository) => {
@@ -205,6 +206,19 @@ export default function AzureProjectCreate(props: Readonly<Props>) {
[onProjectSetupDone, selectedRepository?.projectName],
);
+ const { isSearching, onSearch, onSelectRepository, searchResults } =
+ useProjectRepositorySearch<AzureRepository>({
+ defaultRepositorySelect,
+ fetchData,
+ fetchSearchResults: (query: string, dopKey: string) => searchAzureRepositories(dopKey, query),
+ getRepositoryKey: ({ name }) => name,
+ isMonorepoSetup,
+ selectedDopSetting,
+ setSearchQuery,
+ setSelectedRepository,
+ setShowPersonalAccessTokenForm,
+ });
+
const handleOpenProject = useCallback(
async (projectName: string) => {
if (searchResults !== undefined) {
@@ -224,75 +238,19 @@ export default function AzureProjectCreate(props: Readonly<Props>) {
}));
setRepositories((repositories) => ({ ...repositories, [projectName]: projectRepos }));
},
- [fetchAzureRepositories, searchResults],
+ [fetchAzureRepositories, searchResults, setRepositories],
);
- const handlePersonalAccessTokenCreate = useCallback(() => {
- cleanUrl();
- setShowPersonalAccessTokenForm(false);
- }, [cleanUrl]);
-
- const handleSearchRepositories = useCallback(
- async (searchQuery: string) => {
- if (!selectedDopSetting) {
- return;
- }
-
- if (searchQuery.length === 0) {
- setSearchResults(undefined);
- setSearchQuery('');
- return;
- }
-
- setIsSearching(true);
-
- const searchResults: AzureRepository[] = await searchAzureRepositories(
- selectedDopSetting.key,
- searchQuery,
- )
- .then(({ repositories }) => repositories)
- .catch(() => []);
-
- setIsSearching(false);
- setSearchQuery(searchQuery);
- setSearchResults(searchResults);
- },
- [selectedDopSetting],
- );
-
- const handleSelectRepository = useCallback(
- (repositoryKey: string) => {
- setSelectedRepository(
- Object.values(repositories)
- .flat()
- .find(({ name }) => name === repositoryKey),
- );
- },
- [repositories],
- );
-
- const onSelectedAlmInstanceChange = useCallback(
- (almInstance: AlmSettingsInstance) => {
- setSelectedDopSetting(dopSettings?.find((dopSetting) => dopSetting.key === almInstance.key));
- },
- [dopSettings],
- );
-
- useEffect(() => {
- setSelectedDopSetting(dopSettings?.[0]);
- // We want to update this value only when the list of DOP settings changes from empty to not-empty (or vice-versa)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [hasDopSettings]);
-
- useEffect(() => {
- setSearchResults(undefined);
- setSearchQuery('');
- setShowPersonalAccessTokenForm(true);
- }, [isMonorepoSetup, selectedDopSetting]);
+ const repositoryOptions = useMemo(() => {
+ if (searchResults) {
+ const dict = projects?.reduce((acc: Dict<AzureRepository[]>, { name }) => {
+ return { ...acc, [name]: searchResults?.filter((o) => o.projectName === name) };
+ }, {});
+ return transformToOptions(projects ?? [], dict);
+ }
- useEffect(() => {
- fetchData();
- }, [fetchData]);
+ return transformToOptions(projects ?? [], repositories);
+ }, [projects, repositories, searchResults]);
return isMonorepoSetup ? (
<MonorepoProjectCreate
@@ -300,17 +258,17 @@ export default function AzureProjectCreate(props: Readonly<Props>) {
error={false}
loadingBindings={isLoadingBindings}
loadingOrganizations={false}
- loadingRepositories={isLoading}
+ loadingRepositories={isLoadingRepositories}
onProjectSetupDone={handleMonorepoSetupDone}
- onSearchRepositories={setSearchQuery}
+ onSearchRepositories={onSearch}
onSelectDopSetting={setSelectedDopSetting}
- onSelectRepository={handleSelectRepository}
+ onSelectRepository={onSelectRepository}
personalAccessTokenComponent={
- !isLoading &&
+ !isLoadingRepositories &&
selectedAlmInstance && (
<AzurePersonalAccessTokenForm
almSetting={selectedAlmInstance}
- onPersonalAccessTokenCreate={handlePersonalAccessTokenCreate}
+ onPersonalAccessTokenCreate={handlePersonalAccessTokenCreated}
resetPat={Boolean(location.query.resetPat)}
/>
)
@@ -324,12 +282,12 @@ export default function AzureProjectCreate(props: Readonly<Props>) {
) : (
<AzureCreateProjectRenderer
almInstances={almInstances}
- loading={isLoading || isLoadingBindings}
+ loading={isLoadingRepositories || isLoadingBindings}
loadingRepositories={loadingRepositories}
onImportRepository={handleImportRepository}
onOpenProject={handleOpenProject}
- onPersonalAccessTokenCreate={handlePersonalAccessTokenCreate}
- onSearch={handleSearchRepositories}
+ onPersonalAccessTokenCreate={handlePersonalAccessTokenCreated}
+ onSearch={onSearch}
onSelectedAlmInstanceChange={onSelectedAlmInstanceChange}
projects={projects}
repositories={repositories}
@@ -345,11 +303,14 @@ export default function AzureProjectCreate(props: Readonly<Props>) {
function transformToOptions(
projects: AzureProject[],
- repositories: Dict<AzureRepository[]>,
+ repositories?: Dict<AzureRepository[]>,
): Array<GroupBase<LabelValueSelectOption<string>>> {
return projects.map(({ name: projectName }) => ({
label: projectName,
- options: repositories[projectName]?.map(transformToOption) ?? [],
+ options:
+ repositories?.[projectName] !== undefined
+ ? repositories[projectName].map(transformToOption)
+ : [],
}));
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreateRenderer.tsx
index 42a335f7616..7db5f88a75a 100644
--- a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreateRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreateRenderer.tsx
@@ -51,7 +51,7 @@ export interface AzureProjectCreateRendererProps {
onPersonalAccessTokenCreate: () => void;
onSearch: (query: string) => void;
projects?: AzureProject[];
- repositories: Dict<AzureRepository[]>;
+ repositories?: Dict<AzureRepository[]>;
searching?: boolean;
searchResults?: AzureRepository[];
searchQuery?: string;
diff --git a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectsList.tsx b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectsList.tsx
index 1bdb1c44091..5cd8189660c 100644
--- a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectsList.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectsList.tsx
@@ -34,7 +34,7 @@ export interface AzureProjectsListProps {
onOpenProject: (key: string) => void;
onImportRepository: (repository: AzureRepository) => void;
projects?: AzureProject[];
- repositories: Dict<AzureRepository[]>;
+ repositories?: Dict<AzureRepository[]>;
searchResults?: AzureRepository[];
searchQuery?: string;
}
@@ -121,7 +121,7 @@ export default function AzureProjectsList(props: AzureProjectsListProps) {
repositories={
searchResults
? searchResults.filter((s) => s.projectName === p.name)
- : repositories[p.name]
+ : repositories?.[p.name]
}
searchQuery={searchQuery}
startsOpen={searchResults !== undefined || i === 0}
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx
index 56c40b24f6b..1c206512972 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx
@@ -29,7 +29,7 @@ import { REPOSITORY_PAGE_SIZE } from '../constants';
import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
import { CreateProjectModes } from '../types';
import { useProjectCreate } from '../useProjectCreate';
-import { useProjectRepositorySearch } from '../useProjectRepositorySearch';
+import { useRepositorySearch } from '../useRepositorySearch';
import BitbucketCloudPersonalAccessTokenForm from './BitbucketCloudPersonalAccessTokenForm';
import BitbucketCloudProjectCreateRenderer from './BitbucketCloudProjectCreateRender';
@@ -49,6 +49,7 @@ export default function BitbucketCloudProjectCreate(props: Readonly<Props>) {
});
const {
+ almInstances,
handlePersonalAccessTokenCreated,
handleSelectRepository,
isInitialized,
@@ -61,6 +62,7 @@ export default function BitbucketCloudProjectCreate(props: Readonly<Props>) {
resetLoading,
resetPersonalAccessToken,
searchQuery,
+ selectedAlmInstance,
selectedDopSetting,
selectedRepository,
setIsInitialized,
@@ -69,11 +71,10 @@ export default function BitbucketCloudProjectCreate(props: Readonly<Props>) {
setSearchQuery,
setShowPersonalAccessTokenForm,
showPersonalAccessTokenForm,
- } = useProjectCreate<BitbucketCloudRepository, undefined>(
+ } = useProjectCreate<BitbucketCloudRepository, BitbucketCloudRepository[], undefined>(
AlmKeys.BitbucketCloud,
dopSettings,
({ slug }) => slug,
- REPOSITORY_PAGE_SIZE,
);
const location = useLocation();
@@ -153,7 +154,7 @@ export default function BitbucketCloudProjectCreate(props: Readonly<Props>) {
[onProjectSetupDone, selectedDopSetting],
);
- const { isSearching, onSearch } = useProjectRepositorySearch(
+ const { isSearching, onSearch } = useRepositorySearch(
AlmKeys.BitbucketCloud,
fetchRepositories,
isInitialized,
@@ -192,21 +193,8 @@ export default function BitbucketCloudProjectCreate(props: Readonly<Props>) {
/>
) : (
<BitbucketCloudProjectCreateRenderer
+ almInstances={almInstances}
isLastPage={isLastPage}
- selectedAlmInstance={
- selectedDopSetting
- ? {
- alm: selectedDopSetting.type,
- key: selectedDopSetting.key,
- url: selectedDopSetting.url,
- }
- : undefined
- }
- almInstances={dopSettings?.map((instance) => ({
- alm: instance.type,
- key: instance.key,
- url: instance.url,
- }))}
loadingMore={isLoadingMoreRepositories}
loading={isLoadingRepositories || isLoadingBindings}
onImport={handleImportRepository}
@@ -215,9 +203,10 @@ export default function BitbucketCloudProjectCreate(props: Readonly<Props>) {
onSearch={onSearch}
onSelectedAlmInstanceChange={onSelectedAlmInstanceChange}
repositories={repositories}
+ resetPat={resetPersonalAccessToken || Boolean(location.query.resetPat)}
searching={isSearching}
searchQuery={searchQuery}
- resetPat={resetPersonalAccessToken || Boolean(location.query.resetPat)}
+ selectedAlmInstance={selectedAlmInstance}
showPersonalAccessTokenForm={showPersonalAccessTokenForm || Boolean(location.query.resetPat)}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketImportRepositoryForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketImportRepositoryForm.tsx
index 457c50b8724..dbc28d6e7ee 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketImportRepositoryForm.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketImportRepositoryForm.tsx
@@ -22,11 +22,8 @@ import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { translate } from '../../../../helpers/l10n';
-import {
- BitbucketProject,
- BitbucketProjectRepositories,
- BitbucketRepository,
-} from '../../../../types/alm-integration';
+import { BitbucketProject, BitbucketRepository } from '../../../../types/alm-integration';
+import { Dict } from '../../../../types/types';
import { CreateProjectModes } from '../types';
import BitbucketRepositories from './BitbucketRepositories';
import BitbucketSearchResults from './BitbucketSearchResults';
@@ -35,7 +32,7 @@ export interface BitbucketImportRepositoryFormProps {
onSearch: (query: string) => void;
onImportRepository: (repo: BitbucketRepository) => void;
projects?: BitbucketProject[];
- projectRepositories?: BitbucketProjectRepositories;
+ projectRepositories?: Dict<BitbucketRepository[]>;
searching: boolean;
searchResults?: BitbucketRepository[];
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreate.tsx
index 21067c5e982..a0435481bc5 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreate.tsx
@@ -17,253 +17,244 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import * as React from 'react';
-import { Location, Router } from '~sonar-aligned/types/router';
+import { LabelValueSelectOption } from 'design-system';
+import React, { useCallback, useMemo } from 'react';
+import { GroupBase } from 'react-select';
+import { useLocation } from '~sonar-aligned/components/hoc/withRouter';
import {
getBitbucketServerProjects,
getBitbucketServerRepositories,
searchForBitbucketServerRepositories,
} from '../../../../api/alm-integrations';
-import {
- BitbucketProject,
- BitbucketProjectRepositories,
- BitbucketRepository,
-} from '../../../../types/alm-integration';
-import { AlmSettingsInstance } from '../../../../types/alm-settings';
+import { BitbucketProject, BitbucketRepository } from '../../../../types/alm-integration';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { DopSetting } from '../../../../types/dop-translation';
+import { Dict } from '../../../../types/types';
import { ImportProjectParam } from '../CreateProjectPage';
-import { DEFAULT_BBS_PAGE_SIZE } from '../constants';
+import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
import { CreateProjectModes } from '../types';
+import { useProjectCreate } from '../useProjectCreate';
+import { useProjectRepositorySearch } from '../useProjectRepositorySearch';
import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer';
+import BitbucketServerPersonalAccessTokenForm from './BitbucketServerPersonalAccessTokenForm';
interface Props {
- almInstances: AlmSettingsInstance[];
- loadingBindings: boolean;
- location: Location;
- router: Router;
+ dopSettings: DopSetting[];
+ isLoadingBindings: boolean;
onProjectSetupDone: (importProjects: ImportProjectParam) => void;
}
-interface State {
- selectedAlmInstance?: AlmSettingsInstance;
- loading: boolean;
- projects?: BitbucketProject[];
- projectRepositories?: BitbucketProjectRepositories;
- searching: boolean;
- searchResults?: BitbucketRepository[];
- showPersonalAccessTokenForm: boolean;
-}
-
-export default class BitbucketProjectCreate extends React.PureComponent<Props, State> {
- mounted = false;
+export default function BitbucketProjectCreate({
+ dopSettings,
+ isLoadingBindings,
+ onProjectSetupDone,
+}: Readonly<Props>) {
+ const {
+ almInstances,
+ handlePersonalAccessTokenCreated,
+ handleSelectRepository: defaultRepositorySelect,
+ isLoadingRepositories,
+ isMonorepoSetup,
+ onSelectedAlmInstanceChange,
+ organizations: projects,
+ repositories,
+ resetPersonalAccessToken,
+ searchQuery,
+ selectedAlmInstance,
+ selectedDopSetting,
+ selectedRepository,
+ setIsLoadingRepositories,
+ setOrganizations: setProjects,
+ setRepositories,
+ setSearchQuery,
+ setSelectedDopSetting,
+ setSelectedRepository,
+ setShowPersonalAccessTokenForm,
+ showPersonalAccessTokenForm,
+ } = useProjectCreate<BitbucketRepository, Dict<BitbucketRepository[]>, BitbucketProject>(
+ AlmKeys.BitbucketServer,
+ dopSettings,
+ ({ slug }) => slug,
+ );
- constructor(props: Props) {
- super(props);
- this.state = {
- selectedAlmInstance: props.almInstances[0],
- loading: false,
- searching: false,
- showPersonalAccessTokenForm: true,
- };
- }
+ const location = useLocation();
- 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.fetchInitialData().catch(() => {
- /* noop */
- });
- });
+ const fetchBitbucketProjects = useCallback((): Promise<BitbucketProject[] | undefined> => {
+ if (!selectedDopSetting) {
+ return Promise.resolve(undefined);
}
- }
- componentWillUnmount() {
- this.mounted = false;
- }
+ return getBitbucketServerProjects(selectedDopSetting.key).then(({ projects }) => projects);
+ }, [selectedDopSetting]);
- fetchInitialData = async () => {
- const { showPersonalAccessTokenForm } = this.state;
+ const fetchBitbucketRepositories = useCallback(
+ (projects: BitbucketProject[]): Promise<Dict<BitbucketRepository[]> | undefined> => {
+ if (!selectedDopSetting) {
+ return Promise.resolve(undefined);
+ }
- if (!showPersonalAccessTokenForm) {
- this.setState({ loading: true });
- const projects = await this.fetchBitbucketProjects().catch(() => undefined);
+ return Promise.all(
+ projects.map((p) => {
+ return getBitbucketServerRepositories(selectedDopSetting.key, p.name).then(
+ ({ repositories }) => {
+ // Because the WS uses the project name rather than its key to find
+ // repositories, we can match more repositories than we expect. For
+ // example, p.name = "A1" would find repositories for projects "A1",
+ // "A10", "A11", etc. This is a limitation of BBS. To make sure we
+ // don't display incorrect information, filter on the project key.
+ const filteredRepositories = repositories.filter((r) => r.projectKey === p.key);
- let projectRepositories;
- if (projects && projects.length > 0) {
- projectRepositories = await this.fetchBitbucketRepositories(projects).catch(
- () => undefined,
- );
- }
+ return {
+ repositories: filteredRepositories,
+ projectKey: p.key,
+ };
+ },
+ );
+ }),
+ ).then((results) => {
+ return results.reduce((acc: Dict<BitbucketRepository[]>, { projectKey, repositories }) => {
+ return { ...acc, [projectKey]: repositories };
+ }, {});
+ });
+ },
+ [selectedDopSetting],
+ );
- if (this.mounted) {
- this.setState({
- projects,
- projectRepositories,
- loading: false,
+ const handleImportRepository = useCallback(
+ (selectedRepository: BitbucketRepository) => {
+ if (selectedDopSetting) {
+ onProjectSetupDone({
+ creationMode: CreateProjectModes.BitbucketServer,
+ almSetting: selectedDopSetting.key,
+ monorepo: false,
+ projects: [
+ {
+ projectKey: selectedRepository.projectKey,
+ repositorySlug: selectedRepository.slug,
+ },
+ ],
});
}
- }
- };
+ },
+ [onProjectSetupDone, selectedDopSetting],
+ );
- fetchBitbucketProjects = (): Promise<BitbucketProject[] | undefined> => {
- const { selectedAlmInstance } = this.state;
+ const handleMonorepoSetupDone = useCallback(
+ (monorepoSetup: ImportProjectParam) => {
+ const bitbucketMonorepoSetup = {
+ ...monorepoSetup,
+ projectIdentifier: selectedRepository?.projectKey,
+ };
- if (!selectedAlmInstance) {
- return Promise.resolve(undefined);
- }
+ onProjectSetupDone(bitbucketMonorepoSetup);
+ },
+ [onProjectSetupDone, selectedRepository?.projectKey],
+ );
- return getBitbucketServerProjects(selectedAlmInstance.key).then(({ projects }) => projects);
- };
+ const fetchData = useCallback(async () => {
+ if (!showPersonalAccessTokenForm) {
+ setIsLoadingRepositories(true);
+ const projects = await fetchBitbucketProjects().catch(() => undefined);
- fetchBitbucketRepositories = (
- projects: BitbucketProject[],
- ): Promise<BitbucketProjectRepositories | undefined> => {
- const { selectedAlmInstance } = this.state;
+ let projectRepositories;
+ if (projects && projects.length > 0) {
+ projectRepositories = await fetchBitbucketRepositories(projects).catch(() => undefined);
+ }
- if (!selectedAlmInstance) {
- return Promise.resolve(undefined);
+ setProjects(projects ?? []);
+ setRepositories(projectRepositories ?? {});
+ setIsLoadingRepositories(false);
}
+ }, [
+ fetchBitbucketProjects,
+ fetchBitbucketRepositories,
+ showPersonalAccessTokenForm,
+ setIsLoadingRepositories,
+ setProjects,
+ setRepositories,
+ ]);
- return Promise.all(
- projects.map((p) => {
- return getBitbucketServerRepositories(selectedAlmInstance.key, p.name).then(
- ({ isLastPage, repositories }) => {
- // Because the WS uses the project name rather than its key to find
- // repositories, we can match more repositories than we expect. For
- // example, p.name = "A1" would find repositories for projects "A1",
- // "A10", "A11", etc. This is a limitation of BBS. To make sure we
- // don't display incorrect information, filter on the project key.
- const filteredRepositories = repositories.filter((r) => r.projectKey === p.key);
-
- // And because of the above, the "isLastPage" cannot be relied upon
- // either. This one is impossible to get 100% for now. We can only
- // make some assumptions: by default, the page size for BBS is 25
- // (this is not part of the payload, so we don't know the actual
- // number; but changing this implies changing some advanced config,
- // so it's not likely). If the filtered repos is larger than this
- // number AND isLastPage is false, we'll keep it at false.
- // Otherwise, we assume it's true.
- const realIsLastPage =
- isLastPage || filteredRepositories.length < DEFAULT_BBS_PAGE_SIZE;
-
- return {
- repositories: filteredRepositories,
- isLastPage: realIsLastPage,
- projectKey: p.key,
- };
- },
- );
- }),
- ).then((results) => {
- return results.reduce(
- (acc: BitbucketProjectRepositories, { isLastPage, projectKey, repositories }) => {
- return { ...acc, [projectKey]: { allShown: isLastPage, repositories } };
- },
- {},
- );
- });
- };
-
- cleanUrl = () => {
- const { location, router } = this.props;
- delete location.query.resetPat;
- router.replace(location);
- };
-
- handlePersonalAccessTokenCreated = () => {
- this.cleanUrl();
-
- this.setState({ showPersonalAccessTokenForm: false }, () => {
- this.fetchInitialData();
+ const { isSearching, onSearch, onSelectRepository, searchResults } =
+ useProjectRepositorySearch<BitbucketRepository>({
+ defaultRepositorySelect,
+ fetchData,
+ fetchSearchResults: (query: string, dopKey: string) =>
+ searchForBitbucketServerRepositories(dopKey, query),
+ getRepositoryKey: ({ slug }) => slug,
+ isMonorepoSetup,
+ selectedDopSetting,
+ setSearchQuery,
+ setSelectedRepository,
+ setShowPersonalAccessTokenForm,
});
- };
-
- handleImportRepository = (selectedRepository: BitbucketRepository) => {
- const { selectedAlmInstance } = this.state;
- if (selectedAlmInstance) {
- this.props.onProjectSetupDone({
- creationMode: CreateProjectModes.BitbucketServer,
- almSetting: selectedAlmInstance.key,
- monorepo: false,
- projects: [
- {
- projectKey: selectedRepository.projectKey,
- repositorySlug: selectedRepository.slug,
- },
- ],
- });
+ const repositoryOptions = useMemo(() => {
+ if (searchResults) {
+ const dict = projects?.reduce((acc: Dict<BitbucketRepository[]>, { key }) => {
+ return { ...acc, [key]: searchResults?.filter((o) => o.projectKey === key) };
+ }, {});
+ return transformToOptions(projects ?? [], dict);
}
- };
- handleSearch = (query: string) => {
- const { selectedAlmInstance } = this.state;
+ return transformToOptions(projects ?? [], repositories);
+ }, [projects, repositories, searchResults]);
- if (!selectedAlmInstance) {
- return;
- }
-
- if (!query) {
- this.setState({ searching: false, searchResults: undefined });
- return;
- }
-
- this.setState({ searching: true });
- searchForBitbucketServerRepositories(selectedAlmInstance.key, query)
- .then(({ repositories }) => {
- if (this.mounted) {
- this.setState({ searching: false, searchResults: repositories });
- }
- })
- .catch(() => {
- if (this.mounted) {
- this.setState({ searching: false });
- }
- });
- };
-
- onSelectedAlmInstanceChange = (instance: AlmSettingsInstance) => {
- this.setState({
- selectedAlmInstance: instance,
- showPersonalAccessTokenForm: true,
- searching: false,
- searchResults: undefined,
- });
- };
+ return isMonorepoSetup ? (
+ <MonorepoProjectCreate
+ dopSettings={dopSettings}
+ error={false}
+ loadingBindings={isLoadingBindings}
+ loadingOrganizations={false}
+ loadingRepositories={isLoadingRepositories}
+ onProjectSetupDone={handleMonorepoSetupDone}
+ onSearchRepositories={onSearch}
+ onSelectDopSetting={setSelectedDopSetting}
+ onSelectRepository={onSelectRepository}
+ personalAccessTokenComponent={
+ !isLoadingRepositories &&
+ selectedDopSetting && (
+ <BitbucketServerPersonalAccessTokenForm
+ almSetting={selectedDopSetting}
+ onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated}
+ resetPat={resetPersonalAccessToken}
+ />
+ )
+ }
+ repositoryOptions={repositoryOptions}
+ repositorySearchQuery={searchQuery}
+ selectedDopSetting={selectedDopSetting}
+ selectedRepository={selectedRepository ? transformToOption(selectedRepository) : undefined}
+ showPersonalAccessToken={showPersonalAccessTokenForm || Boolean(location.query.resetPat)}
+ />
+ ) : (
+ <BitbucketCreateProjectRenderer
+ almInstances={almInstances}
+ isLoading={isLoadingRepositories || isLoadingBindings}
+ onImportRepository={handleImportRepository}
+ onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated}
+ onSearch={onSearch}
+ onSelectedAlmInstanceChange={onSelectedAlmInstanceChange}
+ projectRepositories={repositories}
+ projects={projects}
+ resetPat={Boolean(location.query.resetPat)}
+ searchResults={searchResults}
+ searching={isSearching}
+ selectedAlmInstance={selectedAlmInstance}
+ showPersonalAccessTokenForm={showPersonalAccessTokenForm || Boolean(location.query.resetPat)}
+ />
+ );
+}
- render() {
- const { loadingBindings, location, almInstances } = this.props;
- const {
- selectedAlmInstance,
- loading,
- projectRepositories,
- projects,
- searching,
- searchResults,
- showPersonalAccessTokenForm,
- } = this.state;
+function transformToOptions(
+ projects: BitbucketProject[],
+ repositories?: Dict<BitbucketRepository[]>,
+): Array<GroupBase<LabelValueSelectOption<string>>> {
+ return projects.map(({ name, key }) => ({
+ label: name,
+ options: repositories?.[key] !== undefined ? repositories[key].map(transformToOption) : [],
+ }));
+}
- return (
- <BitbucketCreateProjectRenderer
- selectedAlmInstance={selectedAlmInstance}
- almInstances={almInstances}
- loading={loading || loadingBindings}
- onImportRepository={this.handleImportRepository}
- onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated}
- onSearch={this.handleSearch}
- onSelectedAlmInstanceChange={this.onSelectedAlmInstanceChange}
- projectRepositories={projectRepositories}
- projects={projects}
- resetPat={Boolean(location.query.resetPat)}
- searchResults={searchResults}
- searching={searching}
- showPersonalAccessTokenForm={
- showPersonalAccessTokenForm || Boolean(location.query.resetPat)
- }
- />
- );
- }
+function transformToOption({ name, slug }: BitbucketRepository): LabelValueSelectOption<string> {
+ return { value: slug, label: name };
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreateRenderer.tsx
index 961f319f2f4..d44187b2e24 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreateRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreateRenderer.tsx
@@ -17,56 +17,85 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { LightPrimary, PageContentFontWrapper, Spinner, Title } from 'design-system';
-import * as React from 'react';
+import { Link, Spinner } from '@sonarsource/echoes-react';
+import { LightPrimary, PageContentFontWrapper, Title } from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { queryToSearchString } from '~sonar-aligned/helpers/urls';
+import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
import { translate } from '../../../../helpers/l10n';
-import {
- BitbucketProject,
- BitbucketProjectRepositories,
- BitbucketRepository,
-} from '../../../../types/alm-integration';
+import { BitbucketProject, BitbucketRepository } from '../../../../types/alm-integration';
import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
+import { Feature } from '../../../../types/features';
+import { Dict } from '../../../../types/types';
import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown';
import WrongBindingCountAlert from '../components/WrongBindingCountAlert';
+import { CreateProjectModes } from '../types';
import BitbucketImportRepositoryForm from './BitbucketImportRepositoryForm';
import BitbucketServerPersonalAccessTokenForm from './BitbucketServerPersonalAccessTokenForm';
export interface BitbucketProjectCreateRendererProps {
- selectedAlmInstance?: AlmSettingsInstance;
almInstances: AlmSettingsInstance[];
- loading: boolean;
+ isLoading: boolean;
onImportRepository: (repository: BitbucketRepository) => void;
onSearch: (query: string) => void;
onPersonalAccessTokenCreated: () => void;
onSelectedAlmInstanceChange: (instance: AlmSettingsInstance) => void;
projects?: BitbucketProject[];
- projectRepositories?: BitbucketProjectRepositories;
+ projectRepositories?: Dict<BitbucketRepository[]>;
resetPat: boolean;
searching: boolean;
searchResults?: BitbucketRepository[];
+ selectedAlmInstance?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean;
}
-export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCreateRendererProps) {
+export default function BitbucketProjectCreateRenderer(
+ props: Readonly<BitbucketProjectCreateRendererProps>,
+) {
const {
almInstances,
- selectedAlmInstance,
- loading,
+ isLoading,
projects,
projectRepositories,
-
+ resetPat,
searching,
searchResults,
+ selectedAlmInstance,
showPersonalAccessTokenForm,
- resetPat,
} = props;
+ const isMonorepoSupported = React.useContext(AvailableFeaturesContext).includes(
+ Feature.MonoRepositoryPullRequestDecoration,
+ );
+
return (
<PageContentFontWrapper>
<header className="sw-mb-10">
<Title className="sw-mb-4">{translate('onboarding.create_project.bitbucket.title')}</Title>
<LightPrimary className="sw-body-sm">
- {translate('onboarding.create_project.bitbucket.subtitle')}
+ {isMonorepoSupported ? (
+ <FormattedMessage
+ id="onboarding.create_project.bitbucket.subtitle.with_monorepo"
+ values={{
+ monorepoSetupLink: (
+ <Link
+ to={{
+ pathname: '/projects/create',
+ search: queryToSearchString({
+ mode: CreateProjectModes.BitbucketServer,
+ mono: true,
+ }),
+ }}
+ >
+ <FormattedMessage id="onboarding.create_project.subtitle_monorepo_setup_link" />
+ </Link>
+ ),
+ }}
+ />
+ ) : (
+ <FormattedMessage id="onboarding.create_project.bitbucket.subtitle" />
+ )}
</LightPrimary>
</header>
@@ -77,12 +106,12 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr
onChangeConfig={props.onSelectedAlmInstanceChange}
/>
- <Spinner loading={loading}>
- {!loading && !selectedAlmInstance && (
+ <Spinner isLoading={isLoading}>
+ {!isLoading && almInstances && almInstances.length === 0 && !selectedAlmInstance && (
<WrongBindingCountAlert alm={AlmKeys.BitbucketServer} />
)}
- {!loading &&
+ {!isLoading &&
selectedAlmInstance &&
(showPersonalAccessTokenForm ? (
<BitbucketServerPersonalAccessTokenForm
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketRepositories.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketRepositories.tsx
index 24f51ca4686..003e52d1329 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketRepositories.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketRepositories.tsx
@@ -19,17 +19,15 @@
*/
import { uniq, without } from 'lodash';
import * as React from 'react';
-import {
- BitbucketProject,
- BitbucketProjectRepositories,
- BitbucketRepository,
-} from '../../../../types/alm-integration';
+import { BitbucketProject, BitbucketRepository } from '../../../../types/alm-integration';
+import { Dict } from '../../../../types/types';
+import { DEFAULT_BBS_PAGE_SIZE } from '../constants';
import BitbucketProjectAccordion from './BitbucketProjectAccordion';
export interface BitbucketRepositoriesProps {
onImportRepository: (repo: BitbucketRepository) => void;
projects: BitbucketProject[];
- projectRepositories: BitbucketProjectRepositories;
+ projectRepositories: Dict<BitbucketRepository[]>;
}
export default function BitbucketRepositories(props: BitbucketRepositoriesProps) {
@@ -49,7 +47,7 @@ export default function BitbucketRepositories(props: BitbucketRepositoriesProps)
<>
{projects.map((project) => {
const isOpen = openProjectKeys.includes(project.key);
- const { allShown, repositories = [] } = projectRepositories[project.key] || {};
+ const repositories = projectRepositories[project.key] ?? [];
return (
<BitbucketProjectAccordion
@@ -58,7 +56,7 @@ export default function BitbucketRepositories(props: BitbucketRepositoriesProps)
open={isOpen}
project={project}
repositories={repositories}
- showingAllRepositories={allShown}
+ showingAllRepositories={repositories.length < DEFAULT_BBS_PAGE_SIZE}
onImportRepository={props.onImportRepository}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketServerPersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketServerPersonalAccessTokenForm.tsx
index 8d9b8f3b4a4..52bc7809881 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketServerPersonalAccessTokenForm.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketServerPersonalAccessTokenForm.tsx
@@ -30,11 +30,11 @@ import {
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from '../../../../helpers/l10n';
-import { AlmSettingsInstance } from '../../../../types/alm-settings';
+import { AlmInstanceBase } from '../../../../types/alm-settings';
import { usePersonalAccessToken } from '../usePersonalAccessToken';
interface Props {
- almSetting: AlmSettingsInstance;
+ almSetting: AlmInstanceBase;
resetPat: boolean;
onPersonalAccessTokenCreated: () => void;
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
index 56ac38b6b26..1422d315c3f 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
@@ -29,7 +29,7 @@ import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from '../../../app/components/available-features/withAvailableFeatures';
import { translate } from '../../../helpers/l10n';
-import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
+import { AlmKeys } from '../../../types/alm-settings';
import { DopSetting } from '../../../types/dop-translation';
import { Feature } from '../../../types/features';
import AlmBindingDefinitionForm from '../../settings/components/almIntegration/AlmBindingDefinitionForm';
@@ -50,7 +50,7 @@ export interface CreateProjectPageProps extends WithAvailableFeaturesProps {
interface State {
azureSettings: DopSetting[];
- bitbucketSettings: AlmSettingsInstance[];
+ bitbucketSettings: DopSetting[];
bitbucketCloudSettings: DopSetting[];
githubSettings: DopSetting[];
gitlabSettings: DopSetting[];
@@ -192,9 +192,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
.then(({ dopSettings }) => {
this.setState({
azureSettings: dopSettings.filter(({ type }) => type === AlmKeys.Azure),
- bitbucketSettings: dopSettings
- .filter(({ type }) => type === AlmKeys.BitbucketServer)
- .map(({ key, type, url }) => ({ alm: type, key, url })),
+ bitbucketSettings: dopSettings.filter(({ type }) => type === AlmKeys.BitbucketServer),
bitbucketCloudSettings: dopSettings.filter(({ type }) => type === AlmKeys.BitbucketCloud),
githubSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitHub),
gitlabSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitLab),
@@ -250,7 +248,6 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
};
renderProjectCreation(mode?: CreateProjectModes) {
- const { location, router } = this.props;
const {
azureSettings,
bitbucketSettings,
@@ -275,10 +272,8 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
case CreateProjectModes.BitbucketServer: {
return (
<BitbucketProjectCreate
- almInstances={bitbucketSettings}
- loadingBindings={loading}
- location={location}
- router={router}
+ dopSettings={bitbucketSettings}
+ isLoadingBindings={loading}
onProjectSetupDone={this.handleProjectSetupDone}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
index 1c9d5f744ca..9eab22d95f5 100644
--- a/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
@@ -29,7 +29,7 @@ import { REPOSITORY_PAGE_SIZE } from '../constants';
import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
import { CreateProjectModes } from '../types';
import { useProjectCreate } from '../useProjectCreate';
-import { useProjectRepositorySearch } from '../useProjectRepositorySearch';
+import { useRepositorySearch } from '../useRepositorySearch';
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
import { redirectToGithub } from './utils';
@@ -43,6 +43,7 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
const { dopSettings, isLoadingBindings, onProjectSetupDone } = props;
const {
+ almInstances,
handleSelectRepository,
isInitialized,
isLoadingOrganizations,
@@ -54,6 +55,7 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
organizations,
repositories,
searchQuery,
+ selectedAlmInstance,
selectedDopSetting,
selectedRepository,
setIsInitialized,
@@ -65,11 +67,10 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
setSelectedOrganization,
selectedOrganization,
setIsLoadingOrganizations,
- } = useProjectCreate<GithubRepository, GithubOrganization>(
+ } = useProjectCreate<GithubRepository, GithubRepository[], GithubOrganization>(
AlmKeys.GitHub,
dopSettings,
({ key }) => key,
- REPOSITORY_PAGE_SIZE,
);
const [isInError, setIsInError] = useState(false);
@@ -79,10 +80,10 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
const router = useRouter();
const organizationOptions = useMemo(() => {
- return organizations.map(transformToOption);
+ return organizations?.map(transformToOption);
}, [organizations]);
const repositoryOptions = useMemo(() => {
- return repositories.map(transformToOption);
+ return repositories?.map(transformToOption);
}, [repositories]);
const fetchRepositories = useCallback(
@@ -104,7 +105,7 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
.then(({ paging, repositories }) => {
setProjectsPaging(paging);
setRepositories((prevRepositories) =>
- pageIndex === 1 ? repositories : [...prevRepositories, ...repositories],
+ pageIndex === 1 ? repositories : [...(prevRepositories ?? []), ...repositories],
);
setIsInitialized(true);
})
@@ -164,7 +165,7 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
const handleSelectOrganization = useCallback(
(organizationKey: string) => {
setSearchQuery('');
- setSelectedOrganization(organizations.find(({ key }) => key === organizationKey));
+ setSelectedOrganization(organizations?.find(({ key }) => key === organizationKey));
},
[organizations, setSearchQuery, setSelectedOrganization],
);
@@ -201,7 +202,7 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDopSetting]);
- const { onSearch } = useProjectRepositorySearch(
+ const { onSearch } = useRepositorySearch(
AlmKeys.GitHub,
fetchRepositories,
isInitialized,
@@ -232,11 +233,7 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
/>
) : (
<GitHubProjectCreateRenderer
- almInstances={dopSettings.map(({ key, type, url }) => ({
- alm: type,
- key,
- url,
- }))}
+ almInstances={almInstances}
error={isInError}
loadingBindings={isLoadingBindings}
loadingOrganizations={isLoadingOrganizations}
@@ -250,13 +247,7 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
repositories={repositories}
repositoryPaging={projectsPaging}
searchQuery={searchQuery}
- selectedAlmInstance={
- selectedDopSetting && {
- alm: selectedDopSetting.type,
- key: selectedDopSetting.key,
- url: selectedDopSetting.url,
- }
- }
+ selectedAlmInstance={selectedAlmInstance}
selectedOrganization={selectedOrganization}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx
index dc39a57ad88..c5834b26766 100644
--- a/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx
@@ -44,7 +44,7 @@ interface GitHubProjectCreateRendererProps {
onLoadMore: () => void;
onSearch: (q: string) => void;
onSelectOrganization: (key: string) => void;
- organizations: GithubOrganization[];
+ organizations?: GithubOrganization[];
repositories?: GithubRepository[];
repositoryPaging: Paging;
searchQuery: string;
@@ -175,7 +175,7 @@ export default function GitHubProjectCreateRenderer(
<DarkLabel htmlFor="github-choose-organization" className="sw-mb-2">
{translate('onboarding.create_project.github.choose_organization')}
</DarkLabel>
- {organizations.length > 0 ? (
+ {organizations && organizations.length > 0 ? (
<InputSelect
className="sw-w-7/12 sw-mb-9"
size="full"
diff --git a/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx
index d82e0eca756..6b72f0db234 100644
--- a/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx
@@ -29,7 +29,7 @@ import { REPOSITORY_PAGE_SIZE } from '../constants';
import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
import { CreateProjectModes } from '../types';
import { useProjectCreate } from '../useProjectCreate';
-import { useProjectRepositorySearch } from '../useProjectRepositorySearch';
+import { useRepositorySearch } from '../useRepositorySearch';
import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm';
import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer';
@@ -43,6 +43,7 @@ export default function GitlabProjectCreate(props: Readonly<Props>) {
const { dopSettings, isLoadingBindings, onProjectSetupDone } = props;
const {
+ almInstances,
handlePersonalAccessTokenCreated,
handleSelectRepository,
isInitialized,
@@ -54,6 +55,7 @@ export default function GitlabProjectCreate(props: Readonly<Props>) {
repositories,
resetPersonalAccessToken,
searchQuery,
+ selectedAlmInstance,
selectedDopSetting,
selectedRepository,
setIsInitialized,
@@ -64,17 +66,16 @@ export default function GitlabProjectCreate(props: Readonly<Props>) {
setSearchQuery,
setShowPersonalAccessTokenForm,
showPersonalAccessTokenForm,
- } = useProjectCreate<GitlabProject, undefined>(
+ } = useProjectCreate<GitlabProject, GitlabProject[], undefined>(
AlmKeys.GitLab,
dopSettings,
({ id }) => id,
- REPOSITORY_PAGE_SIZE,
);
const location = useLocation();
const repositoryOptions = useMemo(() => {
- return repositories.map(transformToOption);
+ return repositories?.map(transformToOption);
}, [repositories]);
const fetchRepositories = useCallback(
@@ -143,7 +144,7 @@ export default function GitlabProjectCreate(props: Readonly<Props>) {
fetchRepositories(undefined, searchQuery, projectsPaging.pageIndex + 1, true);
}, [fetchRepositories, projectsPaging, searchQuery]);
- const { onSearch } = useProjectRepositorySearch(
+ const { onSearch } = useRepositorySearch(
AlmKeys.GitLab,
fetchRepositories,
isInitialized,
@@ -182,11 +183,7 @@ export default function GitlabProjectCreate(props: Readonly<Props>) {
/>
) : (
<GitlabProjectCreateRenderer
- almInstances={dopSettings.map((dopSetting) => ({
- alm: dopSetting.type,
- key: dopSetting.key,
- url: dopSetting.url,
- }))}
+ almInstances={almInstances}
loading={isLoadingRepositories || isLoadingBindings}
onImport={handleImportRepository}
onLoadMore={handleLoadMore}
@@ -197,13 +194,7 @@ export default function GitlabProjectCreate(props: Readonly<Props>) {
projectsPaging={projectsPaging}
resetPat={resetPersonalAccessToken || Boolean(location.query.resetPat)}
searchQuery={searchQuery}
- selectedAlmInstance={
- selectedDopSetting && {
- alm: selectedDopSetting.type,
- key: selectedDopSetting.key,
- url: selectedDopSetting.url,
- }
- }
+ selectedAlmInstance={selectedAlmInstance}
showPersonalAccessTokenForm={showPersonalAccessTokenForm || Boolean(location.query.resetPat)}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
index 16832af1aaf..c235dce4330 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
@@ -90,7 +90,9 @@ it('should ask for PAT when it is not set yet and show the import project featur
expect(await screen.findByText('onboarding.create_project.azure.title')).toBeInTheDocument();
expect(screen.getByText('alm.configuration.selector.label.alm.azure.long')).toBeInTheDocument();
- expect(screen.getByText('onboarding.create_project.enter_pat')).toBeInTheDocument();
+ await selectEvent.select(ui.instanceSelector.get(), [/conf-azure-1/]);
+
+ expect(await screen.findByText('onboarding.create_project.enter_pat')).toBeInTheDocument();
expect(screen.getByText('onboarding.create_project.pat_form.title')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'save' })).toBeInTheDocument();
@@ -135,11 +137,13 @@ it('should show import project feature when PAT is already set', async () => {
).toBeInTheDocument();
await user.type(ui.searchbox.get(), 'repo 2');
- expect(
- screen.queryByRole('listitem', {
- name: 'Azure repo 1',
- }),
- ).not.toBeInTheDocument();
+ await waitFor(() =>
+ expect(
+ screen.queryByRole('listitem', {
+ name: 'Azure repo 1',
+ }),
+ ).not.toBeInTheDocument(),
+ );
expect(
screen.queryByRole('listitem', {
name: 'Azure repo 3',
@@ -199,7 +203,7 @@ it('should show search filter when PAT is already set', async () => {
await user.click(inputSearch);
await user.keyboard('s');
- expect(searchAzureRepositories).toHaveBeenCalledWith('conf-azure-2', 's');
+ await waitFor(() => expect(searchAzureRepositories).toHaveBeenCalledWith('conf-azure-2', 's'));
// Should search with empty results
almIntegrationHandler.setSearchAzureRepositories([]);
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
index 11f52a278aa..8c372759e8d 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { screen, within } from '@testing-library/react';
+import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
@@ -28,7 +28,9 @@ import DopTranslationServiceMock from '../../../../api/mocks/DopTranslationServi
import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
+import { Feature } from '../../../../types/features';
import CreateProjectPage from '../CreateProjectPage';
+import { CreateProjectModes } from '../types';
jest.mock('../../../../api/alm-integrations');
jest.mock('../../../../api/alm-settings');
@@ -38,7 +40,17 @@ let dopTranslationHandler: DopTranslationServiceMock;
let newCodePeriodHandler: NewCodeDefinitionServiceMock;
const ui = {
+ bitbucketServerOnboardingTitle: byRole('heading', {
+ name: 'onboarding.create_project.bitbucket.title',
+ }),
bitbucketServerCreateProjectButton: byText('onboarding.create_project.select_method.bitbucket'),
+ cancelButton: byRole('button', { name: 'cancel' }),
+ monorepoSetupLink: byRole('link', {
+ name: 'onboarding.create_project.subtitle_monorepo_setup_link',
+ }),
+ monorepoTitle: byRole('heading', {
+ name: 'onboarding.create_project.monorepo.titlealm.bitbucket',
+ }),
personalAccessTokenInput: byRole('textbox', {
name: /onboarding.create_project.enter_pat/,
}),
@@ -73,8 +85,9 @@ it('should ask for PAT when it is not set yet and show the import project featur
expect(screen.getByText('onboarding.create_project.bitbucket.title')).toBeInTheDocument();
expect(await ui.instanceSelector.find()).toBeInTheDocument();
+ await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketserver-1/]);
- expect(screen.getByText('onboarding.create_project.pat_form.title')).toBeInTheDocument();
+ expect(await screen.findByText('onboarding.create_project.pat_form.title')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'save' })).toBeDisabled();
@@ -162,9 +175,11 @@ it('should show search filter when PAT is already set', async () => {
await user.click(inputSearch);
await user.keyboard('search');
- expect(searchForBitbucketServerRepositories).toHaveBeenLastCalledWith(
- 'conf-bitbucketserver-2',
- 'search',
+ await waitFor(() =>
+ expect(searchForBitbucketServerRepositories).toHaveBeenLastCalledWith(
+ 'conf-bitbucketserver-2',
+ 'search',
+ ),
);
});
@@ -179,8 +194,38 @@ it('should show no result message when there are no projects', async () => {
expect(await screen.findByText('onboarding.create_project.no_bbs_projects')).toBeInTheDocument();
});
-function renderCreateProject() {
- renderApp('project/create', <CreateProjectPage />, {
- navigateTo: 'project/create?mode=bitbucket',
+describe('Bitbucket Server monorepo project navigation', () => {
+ it('should be able to access monorepo setup page from Bitbucket Server 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 Server onboarding page from monorepo setup page', async () => {
+ const user = userEvent.setup();
+ renderCreateProject({ isMonorepo: true });
+
+ await user.click(await ui.cancelButton.find());
+
+ expect(ui.bitbucketServerOnboardingTitle.get()).toBeInTheDocument();
+ });
+});
+
+function renderCreateProject({
+ isMonorepo = false,
+}: {
+ isMonorepo?: boolean;
+} = {}) {
+ let queryString = `mode=${CreateProjectModes.BitbucketServer}`;
+ if (isMonorepo) {
+ queryString += '&mono=true';
+ }
+
+ renderApp('projects/create', <CreateProjectPage />, {
+ navigateTo: `projects/create?${queryString}`,
+ featureList: [Feature.MonoRepositoryPullRequestDecoration],
});
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
index e3ecfa2ce84..d9a2b7e8a40 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
@@ -41,29 +41,29 @@ let dopTranslationHandler: DopTranslationServiceMock;
let newCodePeriodHandler: NewCodeDefinitionServiceMock;
const ui = {
- cancelButton: byRole('button', { name: 'cancel' }),
bitbucketCloudCreateProjectButton: byText(
'onboarding.create_project.select_method.bitbucketcloud',
),
bitbucketCloudOnboardingTitle: byRole('heading', {
name: 'onboarding.create_project.bitbucketcloud.title',
}),
+ cancelButton: byRole('button', { name: 'cancel' }),
+ instanceSelector: byLabelText(/alm.configuration.selector.label/),
monorepoSetupLink: byRole('link', {
name: 'onboarding.create_project.subtitle_monorepo_setup_link',
}),
monorepoTitle: byRole('heading', {
name: 'onboarding.create_project.monorepo.titlealm.bitbucketcloud',
}),
+ password: byRole('textbox', {
+ name: /onboarding\.create_project\.bitbucket_cloud\.enter_password/,
+ }),
personalAccessTokenInput: byRole('textbox', {
name: /onboarding.create_project.enter_pat/,
}),
- 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;
diff --git a/server/sonar-web/src/main/js/apps/create/project/useProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/useProjectCreate.tsx
index 1c6c34e3c3e..13486d0d261 100644
--- a/server/sonar-web/src/main/js/apps/create/project/useProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/useProjectCreate.tsx
@@ -20,31 +20,46 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
import { isDefined } from '../../../helpers/types';
+import {
+ AzureRepository,
+ BitbucketCloudRepository,
+ BitbucketRepository,
+ GithubRepository,
+ GitlabProject,
+} from '../../../types/alm-integration';
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,
-) {
+import { Dict, Paging } from '../../../types/types';
+import { REPOSITORY_PAGE_SIZE } from './constants';
+
+type RepoTypes =
+ | AzureRepository
+ | BitbucketRepository
+ | BitbucketCloudRepository
+ | GithubRepository
+ | GitlabProject;
+type RepoCollectionTypes = Dict<RepoTypes[]> | RepoTypes[];
+
+export function useProjectCreate<
+ RepoType extends RepoTypes,
+ RepoCollectionType extends RepoCollectionTypes,
+ GroupType,
+>(almKey: AlmKeys, dopSettings: DopSetting[], getKey: (repo: RepoType) => string) {
const [isInitialized, setIsInitialized] = useState(false);
const [selectedDopSetting, setSelectedDopSetting] = useState<DopSetting>();
const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(true);
- const [organizations, setOrganizations] = useState<GroupType[]>([]);
+ 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 [repositories, setRepositories] = useState<RepoCollectionType>();
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,
+ pageSize: REPOSITORY_PAGE_SIZE,
total: 0,
});
@@ -54,6 +69,26 @@ export function useProjectCreate<RepoType, GroupType>(
const isMonorepoSetup = location.query?.mono === 'true';
const hasDopSettings = useMemo(() => Boolean(dopSettings?.length), [dopSettings]);
+ const almInstances = useMemo(
+ () =>
+ dopSettings?.map((dopSetting) => ({
+ alm: dopSetting.type,
+ key: dopSetting.key,
+ url: dopSetting.url,
+ })) ?? [],
+ [dopSettings],
+ );
+
+ const selectedAlmInstance = useMemo(
+ () =>
+ selectedDopSetting && {
+ alm: selectedDopSetting.type,
+ key: selectedDopSetting.key,
+ url: selectedDopSetting.url,
+ },
+ [selectedDopSetting],
+ );
+
const cleanUrl = useCallback(() => {
delete location.query.resetPat;
router.replace(location);
@@ -70,7 +105,7 @@ export function useProjectCreate<RepoType, GroupType>(
setSelectedDopSetting(setting);
setShowPersonalAccessTokenForm(true);
setOrganizations([]);
- setRepositories([]);
+ setRepositories(undefined);
setSearchQuery('');
}, []);
@@ -93,7 +128,16 @@ export function useProjectCreate<RepoType, GroupType>(
const handleSelectRepository = useCallback(
(repositoryKey: string) => {
- setSelectedRepository(repositories.find((repo) => getKey(repo) === repositoryKey));
+ if (Array.isArray(repositories)) {
+ const repos = repositories as RepoType[];
+ setSelectedRepository(repos.find((repo) => getKey(repo) === repositoryKey));
+ } else {
+ const repos = repositories as Dict<RepoType[]>;
+ const selected = Object.values(repos)
+ .flat()
+ .find((repo) => getKey(repo) === repositoryKey);
+ setSelectedRepository(selected);
+ }
},
[getKey, repositories, setSelectedRepository],
);
@@ -124,6 +168,7 @@ export function useProjectCreate<RepoType, GroupType>(
}, [almKey, dopSettings, hasDopSettings, location, selectedDopSetting, setSelectedDopSetting]);
return {
+ almInstances,
handlePersonalAccessTokenCreated,
handleSelectRepository,
hasDopSettings,
@@ -148,6 +193,7 @@ export function useProjectCreate<RepoType, GroupType>(
setIsLoadingOrganizations,
setProjectsPaging,
setOrganizations,
+ selectedAlmInstance,
selectedOrganization,
setRepositories,
setResetPersonalAccessToken,
diff --git a/server/sonar-web/src/main/js/apps/create/project/useProjectRepositorySearch.tsx b/server/sonar-web/src/main/js/apps/create/project/useProjectRepositorySearch.tsx
index c4b0a5ec7c1..69adbfc788c 100644
--- a/server/sonar-web/src/main/js/apps/create/project/useProjectRepositorySearch.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/useProjectRepositorySearch.tsx
@@ -17,86 +17,94 @@
* 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 { isEmpty } from 'lodash';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { AzureRepository, BitbucketRepository } from '../../../types/alm-integration';
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],
- );
+type RepoTypes = AzureRepository | BitbucketRepository;
- useEffect(() => {
- if (selectedDopSetting && !showPersonalAccessTokenForm && orgValid) {
- if (almKey === AlmKeys.GitHub) {
- fetchRepositories(selectedOrganizationKey);
- } else if (!isInitialized) {
- fetchRepositories();
- }
- }
- }, [
- almKey,
- fetchRepositories,
- isInitialized,
- orgValid,
- selectedDopSetting,
- selectedOrganizationKey,
- showPersonalAccessTokenForm,
- ]);
+export function useProjectRepositorySearch<RepoType extends RepoTypes>({
+ defaultRepositorySelect,
+ fetchData,
+ fetchSearchResults,
+ getRepositoryKey,
+ isMonorepoSetup,
+ selectedDopSetting,
+ setSearchQuery,
+ setSelectedRepository,
+ setShowPersonalAccessTokenForm,
+}: {
+ defaultRepositorySelect: (repositoryKey: string) => void;
+ fetchData: () => void;
+ fetchSearchResults: (query: string, dopKey: string) => Promise<{ repositories: RepoType[] }>;
+ getRepositoryKey: (repo: RepoType) => string;
+ isMonorepoSetup: boolean;
+ selectedDopSetting: DopSetting | undefined;
+ setSearchQuery: (query: string) => void;
+ setSelectedRepository: (repo: RepoType) => void;
+ setShowPersonalAccessTokenForm: (show: boolean) => void;
+}) {
+ const repositorySearchDebounceId = useRef<NodeJS.Timeout | undefined>();
+ const [isSearching, setIsSearching] = useState(false);
+ const [searchResults, setSearchResults] = useState<RepoType[] | undefined>();
const onSearch = useCallback(
(query: string) => {
setSearchQuery(query);
- if (!isInitialized || !orgValid) {
+ if (!selectedDopSetting) {
+ return;
+ }
+
+ if (isEmpty(query)) {
+ setSearchQuery('');
+ setSearchResults(undefined);
return;
}
clearTimeout(repositorySearchDebounceId.current);
repositorySearchDebounceId.current = setTimeout(() => {
setIsSearching(true);
- fetchRepositories(
- almKey === AlmKeys.GitHub ? selectedOrganizationKey : undefined,
- query,
- ).then(
- () => setIsSearching(false),
+ fetchSearchResults(query, selectedDopSetting.key).then(
+ ({ repositories }) => {
+ setIsSearching(false);
+ setSearchResults(repositories);
+ },
() => setIsSearching(false),
);
}, REPOSITORY_SEARCH_DEBOUNCE_TIME);
},
- [
- almKey,
- fetchRepositories,
- isInitialized,
- orgValid,
- repositorySearchDebounceId,
- selectedOrganizationKey,
- setIsSearching,
- setSearchQuery,
- ],
+ [fetchSearchResults, selectedDopSetting, setSearchQuery],
+ );
+
+ const onSelectRepository = useCallback(
+ (repositoryKey: string) => {
+ const repo = searchResults?.find((o) => getRepositoryKey(o) === repositoryKey);
+ if (searchResults && repo) {
+ setSelectedRepository(repo);
+ } else {
+ // If we dont have a set of search results we should look for the repository in the base set of repositories
+ defaultRepositorySelect(repositoryKey);
+ }
+ },
+ [defaultRepositorySelect, getRepositoryKey, searchResults, setSelectedRepository],
);
+ useEffect(() => {
+ setSearchResults(undefined);
+ setSearchQuery('');
+ setShowPersonalAccessTokenForm(true);
+ }, [isMonorepoSetup, selectedDopSetting, setSearchQuery, setShowPersonalAccessTokenForm]);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
return {
isSearching,
onSearch,
+ onSelectRepository,
+ searchResults,
};
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/useRepositorySearch.tsx b/server/sonar-web/src/main/js/apps/create/project/useRepositorySearch.tsx
new file mode 100644
index 00000000000..d8a8fe536a1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/useRepositorySearch.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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 useRepositorySearch(
+ 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,
+ };
+}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index bb0980fecf1..e3f02e76d09 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -4428,6 +4428,7 @@ onboarding.create_project.gitlab.link=See on GitLab
onboarding.create_project.monorepo.no_projects=No projects could be fetch from {almKey}. Contact your system administrator.
onboarding.create_project.bitbucket.title=Bitbucket Server project onboarding
onboarding.create_project.bitbucket.subtitle=Import projects from one of your Bitbucket server workspaces
+onboarding.create_project.bitbucket.subtitle.with_monorepo=Import projects from one of your Bitbucket server workspaces or {monorepoSetupLink}.
onboarding.create_project.x_repositories_selected={count} {count, plural, one {repository} other {repositories}} selected
onboarding.create_project.x_repository_created={count} {count, plural, one {repository} other {repositories}} will be created as {count, plural, one {a project} other {projects}} in SonarQube
onboarding.create_project.please_dont_leave=If you leave the page the import could fail. Are you sure you want to leave?