@@ -55,7 +55,7 @@ interface State { | |||
bitbucketSettings: AlmSettingsInstance[]; | |||
bitbucketCloudSettings: AlmSettingsInstance[]; | |||
githubSettings: DopSetting[]; | |||
gitlabSettings: AlmSettingsInstance[]; | |||
gitlabSettings: DopSetting[]; | |||
loading: boolean; | |||
creatingAlmDefinition?: AlmKeys; | |||
importProjects?: ImportProjectParam; | |||
@@ -202,9 +202,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp | |||
.filter(({ type }) => type === AlmKeys.BitbucketCloud) | |||
.map(({ key, type, url }) => ({ alm: type, key, url })), | |||
githubSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitHub), | |||
gitlabSettings: dopSettings | |||
.filter(({ type }) => type === AlmKeys.GitLab) | |||
.map(({ key, type, url }) => ({ alm: type, key, url })), | |||
gitlabSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitLab), | |||
loading: false, | |||
}); | |||
}) | |||
@@ -324,10 +322,8 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp | |||
return ( | |||
<GitlabProjectCreate | |||
canAdmin={!!canAdmin} | |||
loadingBindings={loading} | |||
location={location} | |||
router={router} | |||
almInstances={gitlabSettings} | |||
dopSettings={gitlabSettings} | |||
isLoadingBindings={loading} | |||
onProjectSetupDone={this.handleProjectSetupDone} | |||
/> | |||
); |
@@ -20,7 +20,6 @@ | |||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||
import { getGithubOrganizations, getGithubRepositories } from '../../../../api/alm-integrations'; | |||
import { useLocation, useRouter } from '../../../../components/hoc/withRouter'; | |||
import { LabelValueSelectOption } from '../../../../helpers/search'; | |||
import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration'; | |||
import { AlmSettingsInstance } from '../../../../types/alm-settings'; | |||
import { DopSetting } from '../../../../types/dop-translation'; | |||
@@ -30,6 +29,7 @@ import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate'; | |||
import { CreateProjectModes } from '../types'; | |||
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer'; | |||
import { redirectToGithub } from './utils'; | |||
import { LabelValueSelectOption } from 'design-system'; | |||
interface Props { | |||
canAdmin: boolean; | |||
@@ -253,6 +253,7 @@ export default function GitHubProjectCreate(props: Readonly<Props>) { | |||
selectedDopSetting={selectedDopSetting} | |||
selectedOrganization={selectedOrganization && transformToOption(selectedOrganization)} | |||
selectedRepository={selectedRepository && transformToOption(selectedRepository)} | |||
showOrganizations | |||
/> | |||
) : ( | |||
<GitHubProjectCreateRenderer | |||
@@ -290,6 +291,6 @@ export default function GitHubProjectCreate(props: Readonly<Props>) { | |||
function transformToOption({ | |||
key, | |||
name, | |||
}: GithubOrganization | GithubRepository): LabelValueSelectOption { | |||
}: GithubOrganization | GithubRepository): LabelValueSelectOption<string> { | |||
return { value: key, label: name }; | |||
} |
@@ -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 { usePersonalAccessToken } from '../usePersonalAccessToken'; | |||
import { AlmInstanceBase } from '../../../../types/alm-settings'; | |||
interface Props { | |||
almSetting: AlmSettingsInstance; | |||
almSetting: AlmInstanceBase; | |||
resetPat: boolean; | |||
onPersonalAccessTokenCreated: () => void; | |||
} |
@@ -17,227 +17,255 @@ | |||
* 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 React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||
import { getGitlabProjects } from '../../../../api/alm-integrations'; | |||
import { Location, Router } from '../../../../components/hoc/withRouter'; | |||
import { GitlabProject } from '../../../../types/alm-integration'; | |||
import { AlmSettingsInstance } from '../../../../types/alm-settings'; | |||
import { AlmInstanceBase } from '../../../../types/alm-settings'; | |||
import { Paging } from '../../../../types/types'; | |||
import { ImportProjectParam } from '../CreateProjectPage'; | |||
import { CreateProjectModes } from '../types'; | |||
import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer'; | |||
import { DopSetting } from '../../../../types/dop-translation'; | |||
import { useLocation, useRouter } from '../../../../components/hoc/withRouter'; | |||
import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate'; | |||
import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm'; | |||
import { orderBy } from 'lodash'; | |||
import { LabelValueSelectOption } from 'design-system'; | |||
interface Props { | |||
canAdmin: boolean; | |||
loadingBindings: boolean; | |||
almInstances: AlmSettingsInstance[]; | |||
location: Location; | |||
router: Router; | |||
isLoadingBindings: boolean; | |||
onProjectSetupDone: (importProjects: ImportProjectParam) => void; | |||
dopSettings: DopSetting[]; | |||
} | |||
interface State { | |||
loading: boolean; | |||
loadingMore: boolean; | |||
projects?: GitlabProject[]; | |||
projectsPaging: Paging; | |||
resetPat: boolean; | |||
searching: boolean; | |||
searchQuery: string; | |||
selectedAlmInstance: AlmSettingsInstance; | |||
showPersonalAccessTokenForm: boolean; | |||
} | |||
const REPOSITORY_PAGE_SIZE = 50; | |||
const REPOSITORY_SEARCH_DEBOUNCE_TIME = 250; | |||
const GITLAB_PROJECTS_PAGESIZE = 20; | |||
export default function GitlabProjectCreate(props: Readonly<Props>) { | |||
const { canAdmin, dopSettings, isLoadingBindings, onProjectSetupDone } = props; | |||
export default class GitlabProjectCreate extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
const repositorySearchDebounceId = useRef<NodeJS.Timeout | undefined>(); | |||
constructor(props: Props) { | |||
super(props); | |||
const [isLoadingRepositories, setIsLoadingRepositories] = useState(false); | |||
const [isLoadingMoreRepositories, setIsLoadingMoreRepositories] = 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); | |||
this.state = { | |||
loading: false, | |||
loadingMore: false, | |||
projectsPaging: { pageIndex: 1, total: 0, pageSize: GITLAB_PROJECTS_PAGESIZE }, | |||
resetPat: false, | |||
showPersonalAccessTokenForm: true, | |||
searching: false, | |||
searchQuery: '', | |||
selectedAlmInstance: props.almInstances[0], | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
const { almInstances } = this.props; | |||
if (prevProps.almInstances.length === 0 && this.props.almInstances.length > 0) { | |||
this.setState({ selectedAlmInstance: almInstances[0] }, () => { | |||
this.fetchInitialData().catch(() => { | |||
/* noop */ | |||
}); | |||
}); | |||
const location = useLocation(); | |||
const router = useRouter(); | |||
const isMonorepoSetup = location.query?.mono === 'true'; | |||
const hasDopSettings = useMemo(() => { | |||
if (dopSettings === undefined) { | |||
return false; | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
return dopSettings.length > 0; | |||
}, [dopSettings]); | |||
const repositoryOptions = useMemo(() => { | |||
return repositories.map(transformToOption); | |||
}, [repositories]); | |||
fetchInitialData = async () => { | |||
const { showPersonalAccessTokenForm } = this.state; | |||
const fetchProjects = useCallback( | |||
(pageIndex = 1, query?: string) => { | |||
if (!selectedDopSetting) { | |||
return Promise.resolve(undefined); | |||
} | |||
// eslint-disable-next-line local-rules/no-api-imports | |||
return getGitlabProjects({ | |||
almSetting: selectedDopSetting.key, | |||
page: pageIndex, | |||
pageSize: REPOSITORY_PAGE_SIZE, | |||
query, | |||
}); | |||
}, | |||
[selectedDopSetting], | |||
); | |||
const fetchInitialData = useCallback(() => { | |||
if (!showPersonalAccessTokenForm) { | |||
this.setState({ loading: true }); | |||
const result = await this.fetchProjects(); | |||
if (this.mounted && result) { | |||
const { projects, projectsPaging } = result; | |||
this.setState({ | |||
loading: false, | |||
projects, | |||
projectsPaging, | |||
setIsLoadingRepositories(true); | |||
fetchProjects() | |||
.then((result) => { | |||
if (result?.projects) { | |||
setIsLoadingRepositories(false); | |||
setRepositories( | |||
isMonorepoSetup | |||
? orderBy(result.projects, [(res) => res.name.toLowerCase()], ['asc']) | |||
: result.projects, | |||
); | |||
setRepositoryPaging(result.projectsPaging); | |||
} else { | |||
setIsLoadingRepositories(false); | |||
} | |||
}) | |||
.catch(() => { | |||
setResetPersonalAccessToken(true); | |||
setShowPersonalAccessTokenForm(true); | |||
}); | |||
} else { | |||
this.setState({ | |||
loading: 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]); | |||
const handleImportRepository = useCallback( | |||
(gitlabProjectId: string) => { | |||
if (selectedDopSetting) { | |||
onProjectSetupDone({ | |||
almSetting: selectedDopSetting.key, | |||
creationMode: CreateProjectModes.GitLab, | |||
monorepo: false, | |||
projects: [{ gitlabProjectId }], | |||
}); | |||
} | |||
} | |||
}; | |||
}, | |||
[onProjectSetupDone, selectedDopSetting], | |||
); | |||
handleError = () => { | |||
if (this.mounted) { | |||
this.setState({ resetPat: true, showPersonalAccessTokenForm: true }); | |||
const handleLoadMore = useCallback(async () => { | |||
setIsLoadingMoreRepositories(true); | |||
const result = await fetchProjects(repositoryPaging.pageIndex + 1, searchQuery); | |||
if (result?.projects) { | |||
setRepositoryPaging(result ? result.projectsPaging : repositoryPaging); | |||
setRepositories(result ? [...repositories, ...result.projects] : repositories); | |||
} | |||
setIsLoadingMoreRepositories(false); | |||
}, [fetchProjects, repositories, repositoryPaging, searchQuery]); | |||
return undefined; | |||
}; | |||
const handleSelectRepository = useCallback( | |||
(repositoryKey: string) => { | |||
setSelectedRepository(repositories.find(({ id }) => id === repositoryKey)); | |||
}, | |||
[repositories], | |||
); | |||
fetchProjects = async (pageIndex = 1, query?: string) => { | |||
const { selectedAlmInstance } = this.state; | |||
if (!selectedAlmInstance) { | |||
return Promise.resolve(undefined); | |||
} | |||
const onSelectDopSetting = useCallback((setting: DopSetting | undefined) => { | |||
setSelectedDopSetting(setting); | |||
setShowPersonalAccessTokenForm(true); | |||
setRepositories([]); | |||
setSearchQuery(''); | |||
}, []); | |||
try { | |||
// eslint-disable-next-line local-rules/no-api-imports | |||
return await getGitlabProjects({ | |||
almSetting: selectedAlmInstance.key, | |||
page: pageIndex, | |||
pageSize: GITLAB_PROJECTS_PAGESIZE, | |||
query, | |||
}); | |||
} catch (_) { | |||
return this.handleError(); | |||
const onSelectedAlmInstanceChange = useCallback( | |||
(instance: AlmInstanceBase) => { | |||
onSelectDopSetting(dopSettings.find((dopSetting) => dopSetting.key === instance.key)); | |||
}, | |||
[dopSettings, onSelectDopSetting], | |||
); | |||
useEffect(() => { | |||
if (dopSettings.length > 0) { | |||
setSelectedDopSetting(dopSettings[0]); | |||
return; | |||
} | |||
}; | |||
handleImport = (gitlabProjectId: string) => { | |||
const { selectedAlmInstance } = this.state; | |||
setSelectedDopSetting(undefined); | |||
// eslint-disable-next-line react-hooks/exhaustive-deps | |||
}, [hasDopSettings]); | |||
if (selectedAlmInstance) { | |||
this.props.onProjectSetupDone({ | |||
creationMode: CreateProjectModes.GitLab, | |||
almSetting: selectedAlmInstance.key, | |||
monorepo: false, | |||
projects: [{ gitlabProjectId }], | |||
}); | |||
} | |||
}; | |||
handleLoadMore = async () => { | |||
this.setState({ loadingMore: true }); | |||
const { | |||
projectsPaging: { pageIndex }, | |||
searchQuery, | |||
} = this.state; | |||
const result = await this.fetchProjects(pageIndex + 1, searchQuery); | |||
if (this.mounted) { | |||
this.setState(({ projects = [], projectsPaging }) => ({ | |||
loadingMore: false, | |||
projects: result ? [...projects, ...result.projects] : projects, | |||
projectsPaging: result ? result.projectsPaging : projectsPaging, | |||
})); | |||
useEffect(() => { | |||
if (selectedDopSetting) { | |||
fetchInitialData(); | |||
} | |||
}; | |||
handleSearch = async (searchQuery: string) => { | |||
this.setState({ searching: true, searchQuery }); | |||
const result = await this.fetchProjects(1, searchQuery); | |||
if (this.mounted) { | |||
this.setState(({ projects, projectsPaging }) => ({ | |||
searching: false, | |||
projects: result ? result.projects : projects, | |||
projectsPaging: result ? result.projectsPaging : projectsPaging, | |||
})); | |||
} | |||
}; | |||
}, [fetchInitialData, selectedDopSetting]); | |||
cleanUrl = () => { | |||
const { location, router } = this.props; | |||
delete location.query.resetPat; | |||
router.replace(location); | |||
}; | |||
handlePersonalAccessTokenCreated = () => { | |||
this.cleanUrl(); | |||
this.setState({ showPersonalAccessTokenForm: false, resetPat: false }, () => { | |||
this.fetchInitialData(); | |||
}); | |||
}; | |||
onSelectedAlmInstanceChange = (instance: AlmSettingsInstance) => { | |||
this.setState({ | |||
selectedAlmInstance: instance, | |||
showPersonalAccessTokenForm: true, | |||
projects: undefined, | |||
resetPat: false, | |||
searchQuery: '', | |||
}); | |||
}; | |||
render() { | |||
const { loadingBindings, location, almInstances, canAdmin } = this.props; | |||
const { | |||
loading, | |||
loadingMore, | |||
projects, | |||
projectsPaging, | |||
resetPat, | |||
searching, | |||
searchQuery, | |||
selectedAlmInstance, | |||
showPersonalAccessTokenForm, | |||
} = this.state; | |||
return ( | |||
<GitlabProjectCreateRenderer | |||
canAdmin={canAdmin} | |||
almInstances={almInstances} | |||
selectedAlmInstance={selectedAlmInstance} | |||
loading={loading || loadingBindings} | |||
loadingMore={loadingMore} | |||
onImport={this.handleImport} | |||
onLoadMore={this.handleLoadMore} | |||
onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated} | |||
onSearch={this.handleSearch} | |||
projects={projects} | |||
projectsPaging={projectsPaging} | |||
resetPat={resetPat || Boolean(location.query.resetPat)} | |||
searching={searching} | |||
searchQuery={searchQuery} | |||
showPersonalAccessTokenForm={ | |||
showPersonalAccessTokenForm || Boolean(location.query.resetPat) | |||
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 ? ( | |||
<MonorepoProjectCreate | |||
canAdmin={canAdmin} | |||
dopSettings={dopSettings} | |||
error={false} | |||
loadingBindings={isLoadingBindings} | |||
loadingOrganizations={false} | |||
loadingRepositories={isLoadingRepositories} | |||
onProjectSetupDone={onProjectSetupDone} | |||
onSearchRepositories={setSearchQuery} | |||
onSelectDopSetting={onSelectDopSetting} | |||
onSelectRepository={handleSelectRepository} | |||
personalAccessTokenComponent={ | |||
!isLoadingRepositories && | |||
selectedDopSetting && ( | |||
<GitlabPersonalAccessTokenForm | |||
almSetting={selectedDopSetting} | |||
resetPat={resetPersonalAccessToken} | |||
onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated} | |||
/> | |||
) | |||
} | |||
repositoryOptions={repositoryOptions} | |||
repositorySearchQuery={searchQuery} | |||
selectedDopSetting={selectedDopSetting} | |||
selectedRepository={selectedRepository ? transformToOption(selectedRepository) : undefined} | |||
showPersonalAccessToken={showPersonalAccessTokenForm || Boolean(location.query.resetPat)} | |||
/> | |||
) : ( | |||
<GitlabProjectCreateRenderer | |||
almInstances={dopSettings.map((dopSetting) => ({ | |||
alm: dopSetting.type, | |||
key: dopSetting.key, | |||
url: dopSetting.url, | |||
}))} | |||
canAdmin={canAdmin} | |||
loading={isLoadingRepositories || isLoadingBindings} | |||
loadingMore={isLoadingMoreRepositories} | |||
onImport={handleImportRepository} | |||
onLoadMore={handleLoadMore} | |||
onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated} | |||
onSearch={setSearchQuery} | |||
onSelectedAlmInstanceChange={onSelectedAlmInstanceChange} | |||
projects={repositories} | |||
projectsPaging={repositoryPaging} | |||
resetPat={resetPersonalAccessToken || Boolean(location.query.resetPat)} | |||
searching={isLoadingRepositories} | |||
searchQuery={searchQuery} | |||
selectedAlmInstance={ | |||
selectedDopSetting && { | |||
alm: selectedDopSetting.type, | |||
key: selectedDopSetting.key, | |||
url: selectedDopSetting.url, | |||
} | |||
onSelectedAlmInstanceChange={this.onSelectedAlmInstanceChange} | |||
/> | |||
); | |||
} | |||
} | |||
showPersonalAccessTokenForm={showPersonalAccessTokenForm || Boolean(location.query.resetPat)} | |||
/> | |||
); | |||
} | |||
function transformToOption({ id, name }: GitlabProject): LabelValueSelectOption<string> { | |||
return { value: id, label: name }; | |||
} |
@@ -17,16 +17,22 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { LightPrimary, Spinner, Title } from 'design-system'; | |||
import { LightPrimary, Title } from 'design-system'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { GitlabProject } from '../../../../types/alm-integration'; | |||
import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings'; | |||
import { AlmInstanceBase, AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings'; | |||
import { Paging } from '../../../../types/types'; | |||
import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown'; | |||
import WrongBindingCountAlert from '../components/WrongBindingCountAlert'; | |||
import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm'; | |||
import GitlabProjectSelectionForm from './GitlabProjectSelectionForm'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link, Spinner } from '@sonarsource/echoes-react'; | |||
import { queryToSearch } from '../../../../helpers/urls'; | |||
import { CreateProjectModes } from '../types'; | |||
import { Feature } from '../../../../types/features'; | |||
import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; | |||
export interface GitlabProjectCreateRendererProps { | |||
canAdmin?: boolean; | |||
@@ -44,10 +50,16 @@ export interface GitlabProjectCreateRendererProps { | |||
almInstances?: AlmSettingsInstance[]; | |||
selectedAlmInstance?: AlmSettingsInstance; | |||
showPersonalAccessTokenForm?: boolean; | |||
onSelectedAlmInstanceChange: (instance: AlmSettingsInstance) => void; | |||
onSelectedAlmInstanceChange: (instance: AlmInstanceBase) => void; | |||
} | |||
export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) { | |||
export default function GitlabProjectCreateRenderer( | |||
props: Readonly<GitlabProjectCreateRendererProps>, | |||
) { | |||
const isMonorepoSupported = React.useContext(AvailableFeaturesContext).includes( | |||
Feature.MonoRepositoryPullRequestDecoration, | |||
); | |||
const { | |||
canAdmin, | |||
loading, | |||
@@ -67,7 +79,28 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe | |||
<header className="sw-mb-10"> | |||
<Title className="sw-mb-4">{translate('onboarding.create_project.gitlab.title')}</Title> | |||
<LightPrimary className="sw-body-sm"> | |||
{translate('onboarding.create_project.gitlab.subtitle')} | |||
{isMonorepoSupported ? ( | |||
<FormattedMessage | |||
id="onboarding.create_project.gitlab.subtitle.with_monorepo" | |||
values={{ | |||
monorepoSetupLink: ( | |||
<Link | |||
to={{ | |||
pathname: '/projects/create', | |||
search: queryToSearch({ | |||
mode: CreateProjectModes.GitLab, | |||
mono: true, | |||
}), | |||
}} | |||
> | |||
<FormattedMessage id="onboarding.create_project.gitlab.subtitle.link" /> | |||
</Link> | |||
), | |||
}} | |||
/> | |||
) : ( | |||
<FormattedMessage id="onboarding.create_project.gitlab.subtitle" /> | |||
)} | |||
</LightPrimary> | |||
</header> | |||
@@ -78,7 +111,7 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe | |||
onChangeConfig={props.onSelectedAlmInstanceChange} | |||
/> | |||
<Spinner loading={loading} /> | |||
<Spinner isLoading={loading} /> | |||
{!loading && !selectedAlmInstance && ( | |||
<WrongBindingCountAlert alm={AlmKeys.GitLab} canAdmin={!!canAdmin} /> |
@@ -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 { FlagMessage, InputSearch, LightPrimary, Link } from 'design-system'; | |||
import { FlagMessage, InputSearch, LightPrimary } from 'design-system'; | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import ListFooter from '../../../../components/controls/ListFooter'; | |||
@@ -29,6 +29,7 @@ import { GitlabProject } from '../../../../types/alm-integration'; | |||
import { Paging } from '../../../../types/types'; | |||
import AlmRepoItem from '../components/AlmRepoItem'; | |||
import { CreateProjectModes } from '../types'; | |||
import { Link } from '@sonarsource/echoes-react'; | |||
export interface GitlabProjectSelectionFormProps { | |||
loadingMore: boolean; | |||
@@ -41,7 +42,9 @@ export interface GitlabProjectSelectionFormProps { | |||
searchQuery: string; | |||
} | |||
export default function GitlabProjectSelectionForm(props: GitlabProjectSelectionFormProps) { | |||
export default function GitlabProjectSelectionForm( | |||
props: Readonly<GitlabProjectSelectionFormProps>, | |||
) { | |||
const { loadingMore, projects = [], projectsPaging, searching, searchQuery } = props; | |||
if (projects.length === 0 && searchQuery.length === 0 && !searching) { |
@@ -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'; | |||
import selectEvent from 'react-select-event'; | |||
@@ -28,6 +28,8 @@ import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitio | |||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||
import { byLabelText, byRole, byText } from '../../../../helpers/testSelector'; | |||
import CreateProjectPage from '../CreateProjectPage'; | |||
import { Feature } from '../../../../types/features'; | |||
import { CreateProjectModes } from '../types'; | |||
jest.mock('../../../../api/alm-integrations'); | |||
jest.mock('../../../../api/alm-settings'); | |||
@@ -37,12 +39,16 @@ let dopTranslationHandler: DopTranslationServiceMock; | |||
let newCodePeriodHandler: NewCodeDefinitionServiceMock; | |||
const ui = { | |||
cancelButton: byRole('button', { name: 'cancel' }), | |||
gitlabCreateProjectButton: byText('onboarding.create_project.select_method.gitlab'), | |||
gitLabOnboardingTitle: byRole('heading', { name: 'onboarding.create_project.gitlab.title' }), | |||
instanceSelector: byLabelText(/alm.configuration.selector.label/), | |||
monorepoSetupLink: byRole('link', { name: 'onboarding.create_project.gitlab.subtitle.link' }), | |||
monorepoTitle: byRole('heading', { name: 'onboarding.create_project.monorepo.titlealm.gitlab' }), | |||
personalAccessTokenInput: byRole('textbox', { | |||
name: /onboarding.create_project.enter_pat/, | |||
}), | |||
instanceSelector: byLabelText(/alm.configuration.selector.label/), | |||
}; | |||
const original = window.location; | |||
@@ -145,17 +151,18 @@ it('should show search filter when PAT is already set', async () => { | |||
await user.click(inputSearch); | |||
await user.keyboard('sea'); | |||
await waitFor(() => expect(getGitlabProjects).toHaveBeenCalledTimes(2)); | |||
expect(getGitlabProjects).toHaveBeenLastCalledWith({ | |||
almSetting: 'conf-final-2', | |||
page: 1, | |||
pageSize: 20, | |||
pageSize: 50, | |||
query: 'sea', | |||
}); | |||
}); | |||
it('should have load more', async () => { | |||
const user = userEvent.setup(); | |||
almIntegrationHandler.createRandomGitlabProjectsWithLoadMore(10, 20); | |||
almIntegrationHandler.createRandomGitlabProjectsWithLoadMore(50, 75); | |||
renderCreateProject(); | |||
expect(await screen.findByText('onboarding.create_project.gitlab.title')).toBeInTheDocument(); | |||
@@ -167,12 +174,12 @@ it('should have load more', async () => { | |||
* Next api call response will simulate reaching the last page so we can test the | |||
* loadmore button disapperance. | |||
*/ | |||
almIntegrationHandler.createRandomGitlabProjectsWithLoadMore(20, 20); | |||
almIntegrationHandler.createRandomGitlabProjectsWithLoadMore(50, 50); | |||
await user.click(loadMore); | |||
expect(getGitlabProjects).toHaveBeenLastCalledWith({ | |||
almSetting: 'conf-final-2', | |||
page: 2, | |||
pageSize: 20, | |||
pageSize: 50, | |||
query: '', | |||
}); | |||
expect(loadMore).not.toBeInTheDocument(); | |||
@@ -190,8 +197,38 @@ it('should show no result message when there are no projects', async () => { | |||
).toBeInTheDocument(); | |||
}); | |||
function renderCreateProject() { | |||
renderApp('project/create', <CreateProjectPage />, { | |||
navigateTo: 'project/create?mode=gitlab', | |||
describe('GitLab monorepo project navigation', () => { | |||
it('should be able to access monorepo setup page from GitLab 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 GitLab onboarding page from monorepo setup page', async () => { | |||
const user = userEvent.setup(); | |||
renderCreateProject({ isMonorepo: true }); | |||
await user.click(await ui.cancelButton.find()); | |||
expect(ui.gitLabOnboardingTitle.get()).toBeInTheDocument(); | |||
}); | |||
}); | |||
function renderCreateProject({ | |||
isMonorepo = false, | |||
}: { | |||
isMonorepo?: boolean; | |||
} = {}) { | |||
let queryString = `mode=${CreateProjectModes.GitLab}`; | |||
if (isMonorepo) { | |||
queryString += '&mono=true'; | |||
} | |||
renderApp('projects/create', <CreateProjectPage />, { | |||
navigateTo: `projects/create?${queryString}`, | |||
featureList: [Feature.MonoRepositoryPullRequestDecoration], | |||
}); | |||
} |
@@ -51,7 +51,7 @@ const ui = { | |||
addButton: byRole('button', { name: 'onboarding.create_project.monorepo.add_project' }), | |||
cancelButton: byRole('button', { name: 'cancel' }), | |||
dopSettingSelector: byRole('combobox', { | |||
name: `onboarding.create_project.monorepo.choose_dop_setting.${AlmKeys.GitHub}`, | |||
name: `onboarding.create_project.monorepo.choose_dop_settingalm.${AlmKeys.GitHub}`, | |||
}), | |||
gitHubOnboardingTitle: byRole('heading', { name: 'onboarding.create_project.github.title' }), | |||
monorepoProjectTitle: byRole('heading', { | |||
@@ -60,11 +60,11 @@ const ui = { | |||
monorepoSetupLink: byRole('link', { name: 'onboarding.create_project.github.subtitle.link' }), | |||
monorepoTitle: byRole('heading', { name: 'onboarding.create_project.monorepo.titlealm.github' }), | |||
organizationSelector: byRole('combobox', { | |||
name: `onboarding.create_project.monorepo.choose_organization.${AlmKeys.GitHub}`, | |||
name: `onboarding.create_project.monorepo.choose_organization`, | |||
}), | |||
removeButton: byRole('button', { name: 'onboarding.create_project.monorepo.remove_project' }), | |||
repositorySelector: byRole('combobox', { | |||
name: `onboarding.create_project.monorepo.choose_repository.${AlmKeys.GitHub}`, | |||
name: `onboarding.create_project.monorepo.choose_repository`, | |||
}), | |||
notBoundRepositoryMessage: byText( | |||
'onboarding.create_project.monorepo.choose_repository.no_already_bound_projects', |
@@ -20,7 +20,7 @@ | |||
import classNames from 'classnames'; | |||
import { DarkLabel, InputSelect, LabelValueSelectOption, Note } from 'design-system'; | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { FormattedMessage, useIntl } from 'react-intl'; | |||
import { OptionProps, SingleValueProps, components } from 'react-select'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
@@ -62,6 +62,8 @@ function orgToOption(alm: DopSetting) { | |||
} | |||
export default function DopSettingDropdown(props: Readonly<DopSettingDropdownProps>) { | |||
const { formatMessage } = useIntl(); | |||
const { almKey, className, dopSettings, onChangeSetting, selectedDopSetting } = props; | |||
if (!dopSettings || dopSettings.length < MIN_SIZE_INSTANCES) { | |||
return null; | |||
@@ -70,7 +72,10 @@ export default function DopSettingDropdown(props: Readonly<DopSettingDropdownPro | |||
return ( | |||
<div className={classNames('sw-flex sw-flex-col', className)}> | |||
<DarkLabel htmlFor="dop-setting-dropdown" className="sw-mb-2"> | |||
<FormattedMessage id={`onboarding.create_project.monorepo.choose_dop_setting.${almKey}`} /> | |||
<FormattedMessage | |||
id="onboarding.create_project.monorepo.choose_dop_setting" | |||
values={{ almKey: formatMessage({ id: `alm.${almKey}` }) }} | |||
/> | |||
</DarkLabel> | |||
<InputSelect |
@@ -0,0 +1,146 @@ | |||
/* | |||
* 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 { Title } from 'design-system/lib'; | |||
import React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import DopSettingDropdown from '../components/DopSettingDropdown'; | |||
import { MonorepoOrganisationSelector } from './MonorepoOrganisationSelector'; | |||
import { MonorepoRepositorySelector } from './MonorepoRepositorySelector'; | |||
import { LabelValueSelectOption } from '../../../../helpers/search'; | |||
import { DopSetting } from '../../../../types/dop-translation'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
import MonorepoNoOrganisations from './MonorepoNoOrganisations'; | |||
interface Props { | |||
almKey: AlmKeys; | |||
alreadyBoundProjects: { | |||
projectId: string; | |||
projectName: string; | |||
}[]; | |||
canAdmin: boolean; | |||
dopSettings: DopSetting[]; | |||
error: boolean; | |||
isFetchingAlreadyBoundProjects: boolean; | |||
isLoadingAlreadyBoundProjects: boolean; | |||
loadingBindings: boolean; | |||
loadingOrganizations?: boolean; | |||
loadingRepositories: boolean; | |||
onSearchRepositories: (query: string) => void; | |||
onSelectDopSetting: (instance: DopSetting) => void; | |||
onSelectOrganization?: (organizationKey: string) => void; | |||
onSelectRepository: (repositoryKey: string) => void; | |||
organizationOptions?: LabelValueSelectOption[]; | |||
personalAccessTokenComponent?: React.ReactNode; | |||
repositoryOptions?: LabelValueSelectOption[]; | |||
repositorySearchQuery: string; | |||
selectedDopSetting?: DopSetting; | |||
selectedOrganization?: LabelValueSelectOption; | |||
selectedRepository?: LabelValueSelectOption; | |||
showPersonalAccessToken?: boolean; | |||
showOrganizations?: boolean; | |||
} | |||
export function MonorepoConnectionSelector({ | |||
almKey, | |||
alreadyBoundProjects, | |||
canAdmin, | |||
dopSettings, | |||
error, | |||
isFetchingAlreadyBoundProjects, | |||
isLoadingAlreadyBoundProjects, | |||
loadingOrganizations, | |||
loadingRepositories, | |||
onSearchRepositories, | |||
onSelectDopSetting, | |||
onSelectOrganization, | |||
onSelectRepository, | |||
organizationOptions, | |||
personalAccessTokenComponent, | |||
repositoryOptions, | |||
repositorySearchQuery, | |||
selectedDopSetting, | |||
selectedOrganization, | |||
selectedRepository, | |||
showPersonalAccessToken, | |||
showOrganizations, | |||
}: Readonly<Props>) { | |||
return ( | |||
<div className="sw-flex sw-flex-col sw-gap-6"> | |||
<Title> | |||
<FormattedMessage | |||
id={ | |||
showOrganizations | |||
? 'onboarding.create_project.monorepo.choose_organization_and_repository' | |||
: 'onboarding.create_project.monorepo.choose_repository' | |||
} | |||
/> | |||
</Title> | |||
<DopSettingDropdown | |||
almKey={almKey} | |||
dopSettings={dopSettings} | |||
selectedDopSetting={selectedDopSetting} | |||
onChangeSetting={onSelectDopSetting} | |||
/> | |||
{showPersonalAccessToken ? ( | |||
personalAccessTokenComponent | |||
) : ( | |||
<> | |||
{showOrganizations && error && selectedDopSetting && !loadingOrganizations && ( | |||
<MonorepoNoOrganisations almKey={almKey} canAdmin={canAdmin} /> | |||
)} | |||
{showOrganizations && organizationOptions && ( | |||
<div className="sw-flex sw-flex-col"> | |||
<MonorepoOrganisationSelector | |||
almKey={almKey} | |||
canAdmin={canAdmin} | |||
error={error} | |||
organizationOptions={organizationOptions} | |||
loadingOrganizations={loadingOrganizations} | |||
onSelectOrganization={onSelectOrganization} | |||
selectedOrganization={selectedOrganization} | |||
/> | |||
</div> | |||
)} | |||
<div className="sw-flex sw-flex-col"> | |||
<MonorepoRepositorySelector | |||
almKey={almKey} | |||
alreadyBoundProjects={alreadyBoundProjects} | |||
error={error} | |||
isFetchingAlreadyBoundProjects={isFetchingAlreadyBoundProjects} | |||
isLoadingAlreadyBoundProjects={isLoadingAlreadyBoundProjects} | |||
loadingRepositories={loadingRepositories} | |||
onSelectRepository={onSelectRepository} | |||
onSearchRepositories={onSearchRepositories} | |||
repositoryOptions={repositoryOptions} | |||
repositorySearchQuery={repositorySearchQuery} | |||
selectedOrganization={selectedOrganization} | |||
selectedRepository={selectedRepository} | |||
showOrganizations={showOrganizations} | |||
/> | |||
</div> | |||
</> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,59 @@ | |||
/* | |||
* 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 { Link } from '@sonarsource/echoes-react'; | |||
import { FlagMessage } from 'design-system/lib'; | |||
import React from 'react'; | |||
import { FormattedMessage, useIntl } from 'react-intl'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
export default function MonorepoNoOrganisations({ | |||
almKey, | |||
canAdmin, | |||
}: Readonly<{ almKey: AlmKeys; canAdmin: boolean }>) { | |||
const { formatMessage } = useIntl(); | |||
return ( | |||
<FlagMessage variant="warning"> | |||
<span> | |||
{canAdmin ? ( | |||
<FormattedMessage | |||
id="onboarding.create_project.monorepo.warning.message_admin" | |||
defaultMessage={formatMessage({ | |||
id: 'onboarding.create_project.monorepo.warning.message_admin', | |||
})} | |||
values={{ | |||
almKey: formatMessage({ id: `alm.${almKey}` }), | |||
link: ( | |||
<Link to="/admin/settings?category=almintegration"> | |||
<FormattedMessage id="onboarding.create_project.monorepo.warning.message_admin.link" /> | |||
</Link> | |||
), | |||
}} | |||
/> | |||
) : ( | |||
<FormattedMessage | |||
id="onboarding.create_project.monorepo.warning.message" | |||
values={{ almKey: formatMessage({ id: `alm.${almKey}` }) }} | |||
/> | |||
)} | |||
</span> | |||
</FlagMessage> | |||
); | |||
} |
@@ -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 { Link, Spinner } from '@sonarsource/echoes-react'; | |||
import { DarkLabel, FlagMessage, InputSelect } from 'design-system'; | |||
import React from 'react'; | |||
import { FormattedMessage, useIntl } from 'react-intl'; | |||
import { LabelValueSelectOption } from '../../../../helpers/search'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
interface Props { | |||
almKey: AlmKeys; | |||
canAdmin: boolean; | |||
error: boolean; | |||
loadingOrganizations?: boolean; | |||
onSelectOrganization?: (organizationKey: string) => void; | |||
organizationOptions: LabelValueSelectOption[]; | |||
selectedOrganization?: LabelValueSelectOption; | |||
} | |||
export function MonorepoOrganisationSelector({ | |||
almKey, | |||
canAdmin, | |||
error, | |||
loadingOrganizations, | |||
onSelectOrganization, | |||
organizationOptions, | |||
selectedOrganization, | |||
}: Readonly<Props>) { | |||
const { formatMessage } = useIntl(); | |||
return ( | |||
!error && ( | |||
<> | |||
<DarkLabel htmlFor={`${almKey}-monorepo-choose-organization`} className="sw-mb-2"> | |||
<FormattedMessage id="onboarding.create_project.monorepo.choose_organization" /> | |||
</DarkLabel> | |||
<Spinner isLoading={loadingOrganizations && !error}> | |||
{organizationOptions.length > 0 ? ( | |||
<InputSelect | |||
size="large" | |||
isSearchable | |||
inputId={`${almKey}-monorepo-choose-organization`} | |||
options={organizationOptions} | |||
onChange={({ value }: LabelValueSelectOption) => { | |||
if (onSelectOrganization) { | |||
onSelectOrganization(value); | |||
} | |||
}} | |||
placeholder={formatMessage({ | |||
id: 'onboarding.create_project.monorepo.choose_organization.placeholder', | |||
})} | |||
value={selectedOrganization} | |||
/> | |||
) : ( | |||
!loadingOrganizations && ( | |||
<FlagMessage variant="error" className="sw-mb-2"> | |||
<span> | |||
{canAdmin ? ( | |||
<FormattedMessage | |||
id="onboarding.create_project.monorepo.no_orgs_admin" | |||
defaultMessage={formatMessage({ | |||
id: 'onboarding.create_project.monorepo.no_orgs_admin', | |||
})} | |||
values={{ | |||
almKey, | |||
link: ( | |||
<Link to="/admin/settings?category=almintegration"> | |||
<FormattedMessage id="onboarding.create_project.monorepo.warning.message_admin.link" /> | |||
</Link> | |||
), | |||
}} | |||
/> | |||
) : ( | |||
<FormattedMessage id="onboarding.create_project.monorepo.no_orgs" /> | |||
)} | |||
</span> | |||
</FlagMessage> | |||
) | |||
)} | |||
</Spinner> | |||
</> | |||
) | |||
); | |||
} |
@@ -17,35 +17,24 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Link, LinkHighlight, LinkStandalone, Spinner } from '@sonarsource/echoes-react'; | |||
import { | |||
AddNewIcon, | |||
BlueGreySeparator, | |||
ButtonPrimary, | |||
ButtonSecondary, | |||
DarkLabel, | |||
FlagMessage, | |||
InputSelect, | |||
SubTitle, | |||
Title, | |||
} from 'design-system'; | |||
import { Spinner } from '@sonarsource/echoes-react'; | |||
import { BlueGreySeparator, ButtonPrimary, ButtonSecondary } from 'design-system'; | |||
import React, { useEffect, useRef } from 'react'; | |||
import { FormattedMessage, useIntl } from 'react-intl'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { getComponents } from '../../../../api/project-management'; | |||
import { useLocation, useRouter } from '../../../../components/hoc/withRouter'; | |||
import { throwGlobalError } from '../../../../helpers/error'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { LabelValueSelectOption } from '../../../../helpers/search'; | |||
import { getProjectUrl } from '../../../../helpers/urls'; | |||
import { useProjectBindingsQuery } from '../../../../queries/dop-translation'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
import { DopSetting } from '../../../../types/dop-translation'; | |||
import { ImportProjectParam } from '../CreateProjectPage'; | |||
import DopSettingDropdown from '../components/DopSettingDropdown'; | |||
import { ProjectData, ProjectValidationCard } from '../components/ProjectValidation'; | |||
import { ProjectData } from '../components/ProjectValidation'; | |||
import { CreateProjectModes } from '../types'; | |||
import { getSanitizedProjectKey } from '../utils'; | |||
import { MonorepoProjectHeader } from './MonorepoProjectHeader'; | |||
import { MonorepoConnectionSelector } from './MonorepoConnectionSelector'; | |||
import { MonorepoProjectsList } from './MonorepoProjectsList'; | |||
interface MonorepoProjectCreateProps { | |||
canAdmin: boolean; | |||
@@ -57,37 +46,29 @@ interface MonorepoProjectCreateProps { | |||
onProjectSetupDone: (importProjects: ImportProjectParam) => void; | |||
onSearchRepositories: (query: string) => void; | |||
onSelectDopSetting: (instance: DopSetting) => void; | |||
onSelectOrganization: (organizationKey: string) => void; | |||
onSelectRepository: (repositoryIdentifier: string) => void; | |||
onSelectOrganization?: (organizationKey: string) => void; | |||
onSelectRepository: (repositoryKey: string) => void; | |||
organizationOptions?: LabelValueSelectOption[]; | |||
personalAccessTokenComponent?: React.ReactNode; | |||
repositoryOptions?: LabelValueSelectOption[]; | |||
repositorySearchQuery: string; | |||
selectedDopSetting?: DopSetting; | |||
selectedOrganization?: LabelValueSelectOption; | |||
selectedRepository?: LabelValueSelectOption; | |||
showOrganizations?: boolean; | |||
showPersonalAccessToken?: boolean; | |||
} | |||
type ProjectItem = Required<ProjectData<number>>; | |||
export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCreateProps>) { | |||
const { | |||
dopSettings, | |||
canAdmin, | |||
error, | |||
loadingBindings, | |||
loadingOrganizations, | |||
loadingRepositories, | |||
onProjectSetupDone, | |||
onSearchRepositories, | |||
onSelectDopSetting, | |||
onSelectOrganization, | |||
onSelectRepository, | |||
organizationOptions, | |||
repositoryOptions, | |||
repositorySearchQuery, | |||
selectedDopSetting, | |||
selectedOrganization, | |||
selectedRepository, | |||
showOrganizations = false, | |||
} = props; | |||
const projectCounter = useRef(0); | |||
@@ -99,7 +80,6 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre | |||
const location = useLocation(); | |||
const { push } = useRouter(); | |||
const { formatMessage } = useIntl(); | |||
const projectKeys = React.useMemo(() => projects.map(({ key }) => key), [projects]); | |||
const { | |||
@@ -116,24 +96,26 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre | |||
const almKey = location.query.mode as AlmKeys; | |||
const isOptionSelectionInvalid = | |||
(showOrganizations && selectedOrganization === undefined) || selectedRepository === undefined; | |||
const isSetupInvalid = | |||
selectedDopSetting === undefined || | |||
selectedOrganization === undefined || | |||
selectedRepository === undefined || | |||
isOptionSelectionInvalid || | |||
projects.length === 0 || | |||
projects.some(({ hasError, key, name }) => hasError || key === '' || name === ''); | |||
const addProject = () => { | |||
if (selectedOrganization === undefined || selectedRepository === undefined) { | |||
const onAddProject = React.useCallback(() => { | |||
if (isOptionSelectionInvalid) { | |||
return; | |||
} | |||
const id = projectCounter.current; | |||
projectCounter.current += 1; | |||
const projectKeySuffix = id === 0 ? '' : `-${id}`; | |||
const projectKey = getSanitizedProjectKey( | |||
`${selectedOrganization.label}_${selectedRepository.label}_add-your-reference${projectKeySuffix}`, | |||
showOrganizations && selectedOrganization | |||
? `${selectedOrganization.label}_${selectedRepository.label}_add-your-reference${projectKeySuffix}` | |||
: `${selectedRepository.label}_add-your-reference${projectKeySuffix}`, | |||
); | |||
const newProjects = [ | |||
@@ -148,28 +130,40 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre | |||
]; | |||
setProjects(newProjects); | |||
}; | |||
const onProjectChange = (project: ProjectItem) => { | |||
const newProjects = projects.filter(({ id }) => id !== project.id); | |||
newProjects.push({ | |||
...project, | |||
}); | |||
newProjects.sort((a, b) => a.id - b.id); | |||
setProjects(newProjects); | |||
}; | |||
}, [ | |||
isOptionSelectionInvalid, | |||
projects, | |||
selectedOrganization, | |||
selectedRepository, | |||
showOrganizations, | |||
]); | |||
const onChangeProject = React.useCallback( | |||
(project: ProjectItem) => { | |||
const newProjects = projects.filter(({ id }) => id !== project.id); | |||
newProjects.push({ | |||
...project, | |||
}); | |||
newProjects.sort((a, b) => a.id - b.id); | |||
setProjects(newProjects); | |||
}, | |||
[projects], | |||
); | |||
const onProjectRemove = (id: number) => { | |||
const newProjects = projects.filter(({ id: projectId }) => projectId !== id); | |||
const onRemoveProject = React.useCallback( | |||
(id: number) => { | |||
const newProjects = projects.filter(({ id: projectId }) => projectId !== id); | |||
setProjects(newProjects); | |||
}; | |||
setProjects(newProjects); | |||
}, | |||
[projects], | |||
); | |||
const cancelMonorepoSetup = () => { | |||
push({ | |||
pathname: location.pathname, | |||
query: { mode: AlmKeys.GitHub }, | |||
query: { mode: almKey }, | |||
}); | |||
}; | |||
@@ -194,7 +188,7 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre | |||
useEffect(() => { | |||
if (selectedRepository !== undefined && projects.length === 0) { | |||
addProject(); | |||
onAddProject(); | |||
} | |||
// eslint-disable-next-line react-hooks/exhaustive-deps | |||
}, [selectedRepository]); | |||
@@ -233,188 +227,25 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre | |||
<BlueGreySeparator className="sw-my-5" /> | |||
<div className="sw-flex sw-flex-col sw-gap-6"> | |||
<Title> | |||
<FormattedMessage | |||
id={`onboarding.create_project.monorepo.choose_organization_and_repository.${almKey}`} | |||
/> | |||
</Title> | |||
<DopSettingDropdown | |||
almKey={almKey} | |||
dopSettings={dopSettings} | |||
selectedDopSetting={selectedDopSetting} | |||
onChangeSetting={onSelectDopSetting} | |||
/> | |||
{error && selectedDopSetting && !loadingOrganizations && ( | |||
<FlagMessage variant="warning"> | |||
<span> | |||
{canAdmin ? ( | |||
<FormattedMessage | |||
id="onboarding.create_project.github.warning.message_admin" | |||
defaultMessage={translate( | |||
'onboarding.create_project.github.warning.message_admin', | |||
)} | |||
values={{ | |||
link: ( | |||
<Link to="/admin/settings?category=almintegration"> | |||
{translate('onboarding.create_project.github.warning.message_admin.link')} | |||
</Link> | |||
), | |||
}} | |||
/> | |||
) : ( | |||
translate('onboarding.create_project.github.warning.message') | |||
)} | |||
</span> | |||
</FlagMessage> | |||
)} | |||
<div className="sw-flex sw-flex-col"> | |||
<Spinner isLoading={loadingOrganizations && !error}> | |||
{!error && ( | |||
<> | |||
<DarkLabel htmlFor="monorepo-choose-organization" className="sw-mb-2"> | |||
<FormattedMessage | |||
id={`onboarding.create_project.monorepo.choose_organization.${almKey}`} | |||
/> | |||
</DarkLabel> | |||
{(organizationOptions?.length ?? 0) > 0 ? ( | |||
<InputSelect | |||
size="full" | |||
isSearchable | |||
inputId="monorepo-choose-organization" | |||
options={organizationOptions} | |||
onChange={({ value }: LabelValueSelectOption) => { | |||
onSelectOrganization(value); | |||
}} | |||
placeholder={formatMessage({ | |||
id: `onboarding.create_project.monorepo.choose_organization.${almKey}.placeholder`, | |||
})} | |||
value={selectedOrganization} | |||
/> | |||
) : ( | |||
!loadingOrganizations && ( | |||
<FlagMessage variant="error" className="sw-mb-2"> | |||
<span> | |||
{canAdmin ? ( | |||
<FormattedMessage | |||
id="onboarding.create_project.github.no_orgs_admin" | |||
defaultMessage={translate( | |||
'onboarding.create_project.github.no_orgs_admin', | |||
)} | |||
values={{ | |||
link: ( | |||
<Link to="/admin/settings?category=almintegration"> | |||
{translate( | |||
'onboarding.create_project.github.warning.message_admin.link', | |||
)} | |||
</Link> | |||
), | |||
}} | |||
/> | |||
) : ( | |||
translate('onboarding.create_project.github.no_orgs') | |||
)} | |||
</span> | |||
</FlagMessage> | |||
) | |||
)} | |||
</> | |||
)} | |||
</Spinner> | |||
</div> | |||
<div className="sw-flex sw-flex-col"> | |||
{selectedOrganization && ( | |||
<DarkLabel className="sw-mb-2" htmlFor="monorepo-choose-repository"> | |||
<FormattedMessage | |||
id={`onboarding.create_project.monorepo.choose_repository.${almKey}`} | |||
/> | |||
</DarkLabel> | |||
)} | |||
{selectedOrganization && ( | |||
<> | |||
<InputSelect | |||
inputId="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.${almKey}.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> | |||
)} | |||
</> | |||
)} | |||
</div> | |||
</div> | |||
<MonorepoConnectionSelector | |||
almKey={almKey} | |||
alreadyBoundProjects={alreadyBoundProjects} | |||
isFetchingAlreadyBoundProjects={isFetchingAlreadyBoundProjects} | |||
isLoadingAlreadyBoundProjects={isLoadingAlreadyBoundProjects} | |||
{...props} | |||
/> | |||
{selectedRepository !== undefined && ( | |||
<> | |||
<BlueGreySeparator className="sw-my-5" /> | |||
<div> | |||
<SubTitle> | |||
<FormattedMessage id="onboarding.create_project.monorepo.project_title" /> | |||
</SubTitle> | |||
<div> | |||
{projects.map(({ id, key, name }) => ( | |||
<ProjectValidationCard | |||
className="sw-mt-4" | |||
initialKey={key} | |||
initialName={name} | |||
key={id} | |||
monorepoSetupProjectKeys={projectKeys} | |||
onChange={onProjectChange} | |||
onRemove={() => { | |||
onProjectRemove(id); | |||
}} | |||
projectId={id} | |||
/> | |||
))} | |||
</div> | |||
<div className="sw-flex sw-justify-end sw-mt-4"> | |||
<ButtonSecondary onClick={addProject}> | |||
<AddNewIcon className="sw-mr-2" /> | |||
<FormattedMessage id="onboarding.create_project.monorepo.add_project" /> | |||
</ButtonSecondary> | |||
</div> | |||
</div> | |||
<MonorepoProjectsList | |||
projectKeys={projectKeys} | |||
onAddProject={onAddProject} | |||
onChangeProject={onChangeProject} | |||
onRemoveProject={onRemoveProject} | |||
projects={projects} | |||
/> | |||
</> | |||
)} | |||
@@ -0,0 +1,69 @@ | |||
/* | |||
* 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 { ButtonSecondary, SubTitle } from 'design-system'; | |||
import React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { ProjectData, ProjectValidationCard } from '../components/ProjectValidation'; | |||
interface Props { | |||
projectKeys: string[]; | |||
onAddProject: () => void; | |||
onChangeProject: (project: ProjectData<number>) => void; | |||
onRemoveProject: (id?: number) => void; | |||
projects: ProjectData<number>[]; | |||
} | |||
export function MonorepoProjectsList({ | |||
projectKeys, | |||
onAddProject, | |||
onChangeProject, | |||
onRemoveProject, | |||
projects, | |||
}: Readonly<Props>) { | |||
return ( | |||
<div> | |||
<SubTitle> | |||
<FormattedMessage id="onboarding.create_project.monorepo.project_title" /> | |||
</SubTitle> | |||
<div> | |||
{projects.map(({ id, key, name }) => ( | |||
<ProjectValidationCard | |||
className="sw-mt-4" | |||
initialKey={key} | |||
initialName={name} | |||
key={id} | |||
monorepoSetupProjectKeys={projectKeys} | |||
onChange={onChangeProject} | |||
onRemove={() => { | |||
onRemoveProject(id); | |||
}} | |||
projectId={id} | |||
/> | |||
))} | |||
</div> | |||
<div className="sw-flex sw-justify-end sw-mt-4"> | |||
<ButtonSecondary onClick={onAddProject}> | |||
<FormattedMessage id="onboarding.create_project.monorepo.add_project" /> | |||
</ButtonSecondary> | |||
</div> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,135 @@ | |||
/* | |||
* 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 { LinkHighlight, LinkStandalone, Spinner } from '@sonarsource/echoes-react'; | |||
import { DarkLabel, FlagMessage, InputSelect } from 'design-system'; | |||
import React from 'react'; | |||
import { FormattedMessage, useIntl } from 'react-intl'; | |||
import { LabelValueSelectOption } from '../../../../helpers/search'; | |||
import { getProjectUrl } from '../../../../helpers/urls'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
interface Props { | |||
almKey: AlmKeys; | |||
alreadyBoundProjects: { | |||
projectId: string; | |||
projectName: string; | |||
}[]; | |||
error: boolean; | |||
isFetchingAlreadyBoundProjects: boolean; | |||
isLoadingAlreadyBoundProjects: boolean; | |||
loadingRepositories: boolean; | |||
onSearchRepositories: (query: string) => void; | |||
onSelectRepository: (repositoryKey: string) => void; | |||
repositorySearchQuery: string; | |||
repositoryOptions?: LabelValueSelectOption[]; | |||
selectedOrganization?: LabelValueSelectOption; | |||
selectedRepository?: LabelValueSelectOption; | |||
showOrganizations?: boolean; | |||
} | |||
export function MonorepoRepositorySelector({ | |||
almKey, | |||
alreadyBoundProjects, | |||
error, | |||
isFetchingAlreadyBoundProjects, | |||
isLoadingAlreadyBoundProjects, | |||
loadingRepositories, | |||
onSearchRepositories, | |||
onSelectRepository, | |||
repositorySearchQuery, | |||
repositoryOptions, | |||
selectedOrganization, | |||
selectedRepository, | |||
showOrganizations, | |||
}: Readonly<Props>) { | |||
const { formatMessage } = useIntl(); | |||
const repositorySelectorEnabled = | |||
!error && | |||
!loadingRepositories && | |||
((showOrganizations && !!selectedOrganization) || !showOrganizations); | |||
const showWarningMessage = | |||
error || (repositorySelectorEnabled && repositoryOptions && repositoryOptions.length === 0); | |||
return ( | |||
<> | |||
<DarkLabel htmlFor={`${almKey}-monorepo-choose-repository`} className="sw-mb-2"> | |||
<FormattedMessage id="onboarding.create_project.monorepo.choose_repository" /> | |||
</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}` }), | |||
}} | |||
/> | |||
) : ( | |||
<> | |||
<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> | |||
</> | |||
); | |||
} |
@@ -23,8 +23,8 @@ import { | |||
setAlmPersonalAccessToken, | |||
} from '../../../api/alm-integrations'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { AlmSettingsInstance } from '../../../types/alm-settings'; | |||
import { tokenExistedBefore } from './utils'; | |||
import { AlmInstanceBase } from '../../../types/alm-settings'; | |||
export interface PATType { | |||
validationFailed: boolean; | |||
@@ -41,7 +41,7 @@ export interface PATType { | |||
} | |||
export const usePersonalAccessToken = ( | |||
almSetting: AlmSettingsInstance, | |||
almSetting: AlmInstanceBase, | |||
resetPat: boolean, | |||
onPersonalAccessTokenCreated: () => void, | |||
): PATType => { |
@@ -21,21 +21,21 @@ import { InputSelect, LabelValueSelectOption, Note } from 'design-system'; | |||
import * as React from 'react'; | |||
import { OptionProps, SingleValueProps, components } from 'react-select'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { AlmSettingsInstance } from '../../types/alm-settings'; | |||
import { AlmInstanceBase } from '../../types/alm-settings'; | |||
function optionRenderer(props: OptionProps<LabelValueSelectOption<AlmSettingsInstance>, false>) { | |||
function optionRenderer(props: OptionProps<LabelValueSelectOption<AlmInstanceBase>, false>) { | |||
return <components.Option {...props}>{customOptions(props.data.value)}</components.Option>; | |||
} | |||
function singleValueRenderer( | |||
props: SingleValueProps<LabelValueSelectOption<AlmSettingsInstance>, false>, | |||
props: SingleValueProps<LabelValueSelectOption<AlmInstanceBase>, false>, | |||
) { | |||
return ( | |||
<components.SingleValue {...props}>{customOptions(props.data.value)}</components.SingleValue> | |||
); | |||
} | |||
function customOptions(instance: AlmSettingsInstance) { | |||
function customOptions(instance: AlmInstanceBase) { | |||
return instance.url ? ( | |||
<> | |||
<span>{instance.key} — </span> | |||
@@ -46,14 +46,14 @@ function customOptions(instance: AlmSettingsInstance) { | |||
); | |||
} | |||
function orgToOption(alm: AlmSettingsInstance) { | |||
function orgToOption(alm: AlmInstanceBase) { | |||
return { value: alm, label: alm.key }; | |||
} | |||
interface Props { | |||
instances: AlmSettingsInstance[]; | |||
instances: AlmInstanceBase[]; | |||
initialValue?: string; | |||
onChange: (instance: AlmSettingsInstance) => void; | |||
onChange: (instance: AlmInstanceBase) => void; | |||
className: string; | |||
inputId: string; | |||
} | |||
@@ -68,7 +68,7 @@ export default function AlmSettingsInstanceSelector(props: Props) { | |||
isClearable={false} | |||
isSearchable={false} | |||
options={instances.map(orgToOption)} | |||
onChange={(data: LabelValueSelectOption<AlmSettingsInstance>) => { | |||
onChange={(data: LabelValueSelectOption<AlmInstanceBase>) => { | |||
props.onChange(data.value); | |||
}} | |||
components={{ | |||
@@ -76,7 +76,7 @@ export default function AlmSettingsInstanceSelector(props: Props) { | |||
SingleValue: singleValueRenderer, | |||
}} | |||
placeholder={translate('alm.configuration.selector.placeholder')} | |||
getOptionValue={(opt: LabelValueSelectOption<AlmSettingsInstance>) => opt.value.key} | |||
getOptionValue={(opt: LabelValueSelectOption<AlmInstanceBase>) => opt.value.key} | |||
value={instances.map(orgToOption).find((opt) => opt.value.key === initialValue) ?? null} | |||
size="full" | |||
/> |
@@ -130,8 +130,11 @@ export interface GitlabProjectAlmBindingParams extends ProjectAlmBindingParams { | |||
repository?: string; | |||
} | |||
export interface AlmSettingsInstance { | |||
export interface AlmSettingsInstance extends AlmInstanceBase { | |||
alm: AlmKeys; | |||
} | |||
export interface AlmInstanceBase { | |||
key: string; | |||
url?: string; | |||
} |
@@ -18,14 +18,12 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { AlmKeys } from './alm-settings'; | |||
import { AlmInstanceBase, AlmKeys } from './alm-settings'; | |||
export interface DopSetting { | |||
export interface DopSetting extends AlmInstanceBase { | |||
appId?: string; | |||
id: string; | |||
key: string; | |||
type: AlmKeys; | |||
url?: string; | |||
} | |||
export interface BoundProject { |
@@ -4420,6 +4420,8 @@ onboarding.create_project.github.no_orgs_admin=We couldn't load any organization | |||
onboarding.create_project.github.no_projects=No projects could be fetched from GitHub. Contact your system administrator. | |||
onboarding.create_project.gitlab.title=Gitlab project onboarding | |||
onboarding.create_project.gitlab.subtitle=Import projects from one of your GitLab groups | |||
onboarding.create_project.gitlab.subtitle.with_monorepo=Import projects from one of your GitLab groups or {monorepoSetupLink}. | |||
onboarding.create_project.gitlab.subtitle.link=set up a monorepo | |||
onboarding.create_project.gitlab.no_projects=No projects could be fetched from Gitlab. Contact your system administrator, or {link}. | |||
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. | |||
@@ -4433,17 +4435,23 @@ onboarding.create_project.import_in_progress={count} of {total} projects importe | |||
onboarding.create_project.monorepo.title={almName} monorepo project onboarding | |||
onboarding.create_project.monorepo.subtitle=Create multiple SonarQube projects corresponding to the same monorepo and bound to the same repository. | |||
onboarding.create_project.monorepo.doc_link=Learn more and get help setting up your monorepo | |||
onboarding.create_project.monorepo.choose_organization_and_repository.github=Choose the organization and the repository | |||
onboarding.create_project.monorepo.choose_dop_setting.github=Choose the GitHub configuration | |||
onboarding.create_project.monorepo.choose_organization.github=Choose the organization | |||
onboarding.create_project.monorepo.choose_organization.github.placeholder=List of organizations | |||
onboarding.create_project.monorepo.choose_repository.github=Choose the repository | |||
onboarding.create_project.monorepo.choose_repository.github.placeholder=List of repositories | |||
onboarding.create_project.monorepo.choose_organization_and_repository=Choose the organization and the repository | |||
onboarding.create_project.monorepo.choose_dop_setting=Choose the {almKey} configuration | |||
onboarding.create_project.monorepo.choose_organization=Choose the organization | |||
onboarding.create_project.monorepo.choose_organization.placeholder=List of organizations | |||
onboarding.create_project.monorepo.choose_repository=Choose the repository | |||
onboarding.create_project.monorepo.choose_repository.placeholder=List of repositories | |||
onboarding.create_project.monorepo.choose_repository.no_already_bound_projects=This repository has no imported projects in SonarQube | |||
onboarding.create_project.monorepo.choose_repository.existing_already_bound_projects=This repository has already been imported, and it's linked to these projects in SonarQube: | |||
onboarding.create_project.monorepo.no_orgs=We couldn't load any organizations with your key. Contact an administrator. | |||
onboarding.create_project.monorepo.no_orgs_admin=We couldn't load any organizations. Make sure the {almKey} App is installed in at least one organization and check the {almKey} instance configuration in the {link}. | |||
onboarding.create_project.monorepo.no_projects=No projects could be fetch from {almKey}. Contact your system administrator. | |||
onboarding.create_project.monorepo.project_title=Create new projects | |||
onboarding.create_project.monorepo.add_project=Add new project | |||
onboarding.create_project.monorepo.remove_project=Remove project | |||
onboarding.create_project.monorepo.warning.message=Could not connect to {almKey}. Please contact an administrator to configure {almKey} integration. | |||
onboarding.create_project.monorepo.warning.message_admin=Could not connect to {almKey}. Please make sure the {almKey} instance is correctly configured in the {link} to create a new project from a repository. | |||
onboarding.create_project.monorepo.warning.message_admin.link=DevOps Platform integration settings | |||
onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code | |||
onboarding.create_x_project.new_code_definition.title=Set up {count, plural, one {project} other {# projects}} for Clean as You Code |