@@ -0,0 +1,86 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { cloneDeep } from 'lodash'; | |||
import { mockGitlabProject } from '../../helpers/mocks/alm-integrations'; | |||
import { GitlabProject } from '../../types/alm-integration'; | |||
import { | |||
checkPersonalAccessTokenIsValid, | |||
getGitlabProjects, | |||
setAlmPersonalAccessToken, | |||
} from '../alm-integrations'; | |||
export default class AlmIntegrationsServiceMock { | |||
almInstancePATMap: { [key: string]: boolean } = {}; | |||
gitlabProjects: GitlabProject[]; | |||
defaultAlmInstancePATMap: { [key: string]: boolean } = { | |||
'conf-final-1': false, | |||
'conf-final-2': true, | |||
}; | |||
defaultGitlabProjects: GitlabProject[] = [ | |||
mockGitlabProject({ | |||
name: 'Gitlab project 1', | |||
id: '1', | |||
sqProjectKey: 'key', | |||
sqProjectName: 'Gitlab project 1', | |||
}), | |||
mockGitlabProject({ name: 'Gitlab project 2', id: '2' }), | |||
mockGitlabProject({ name: 'Gitlab project 3', id: '3' }), | |||
]; | |||
constructor() { | |||
this.almInstancePATMap = cloneDeep(this.defaultAlmInstancePATMap); | |||
this.gitlabProjects = cloneDeep(this.defaultGitlabProjects); | |||
(checkPersonalAccessTokenIsValid as jest.Mock).mockImplementation( | |||
this.checkPersonalAccessTokenIsValid | |||
); | |||
(setAlmPersonalAccessToken as jest.Mock).mockImplementation(this.setAlmPersonalAccessToken); | |||
(getGitlabProjects as jest.Mock).mockImplementation(this.getGitlabProjects); | |||
} | |||
checkPersonalAccessTokenIsValid = (conf: string) => { | |||
return Promise.resolve({ status: this.almInstancePATMap[conf] }); | |||
}; | |||
setAlmPersonalAccessToken = (conf: string) => { | |||
this.almInstancePATMap[conf] = true; | |||
return Promise.resolve(); | |||
}; | |||
getGitlabProjects = () => { | |||
return Promise.resolve({ | |||
projects: this.gitlabProjects, | |||
projectsPaging: { | |||
pageIndex: 1, | |||
pageSize: 30, | |||
total: 3, | |||
}, | |||
}); | |||
}; | |||
setGitlabProjects(gitlabProjects: GitlabProject[]) { | |||
this.gitlabProjects = gitlabProjects; | |||
} | |||
reset = () => { | |||
this.almInstancePATMap = cloneDeep(this.defaultAlmInstancePATMap); | |||
this.gitlabProjects = cloneDeep(this.defaultGitlabProjects); | |||
}; | |||
} |
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { cloneDeep } from 'lodash'; | |||
import { mockAlmSettingsInstance } from '../../helpers/mocks/alm-settings'; | |||
import { AlmKeys, AlmSettingsInstance } from '../../types/alm-settings'; | |||
import { getAlmSettings } from '../alm-settings'; | |||
export default class AlmSettingsServiceMock { | |||
almSettings: AlmSettingsInstance[]; | |||
defaultSetting: AlmSettingsInstance[] = [ | |||
mockAlmSettingsInstance({ key: 'conf-final-1', alm: AlmKeys.GitLab }), | |||
mockAlmSettingsInstance({ key: 'conf-final-2', alm: AlmKeys.GitLab }), | |||
]; | |||
constructor() { | |||
this.almSettings = cloneDeep(this.defaultSetting); | |||
(getAlmSettings as jest.Mock).mockImplementation(this.getAlmSettingsHandler); | |||
} | |||
getAlmSettingsHandler = () => { | |||
return Promise.resolve(this.almSettings); | |||
}; | |||
reset = () => { | |||
this.almSettings = cloneDeep(this.defaultSetting); | |||
}; | |||
} |
@@ -28,6 +28,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/system'; | |||
import { AlmKeys } from '../../../types/alm-settings'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { ALLOWED_MULTIPLE_CONFIGS } from './constants'; | |||
import { CreateProjectModes } from './types'; | |||
export interface CreateProjectModeSelectionProps { | |||
@@ -42,6 +43,32 @@ export interface CreateProjectModeSelectionProps { | |||
const DEFAULT_ICON_SIZE = 50; | |||
function getErrorMessage( | |||
hasTooManyConfig: boolean, | |||
hasConfig: boolean, | |||
canAdmin: boolean | undefined, | |||
alm: AlmKeys | |||
) { | |||
if (hasTooManyConfig) { | |||
return translateWithParameters( | |||
'onboarding.create_project.too_many_alm_instances_X', | |||
translate('alm', alm) | |||
); | |||
} else if (!hasConfig) { | |||
return canAdmin | |||
? translate('onboarding.create_project.alm_not_configured.admin') | |||
: translate('onboarding.create_project.alm_not_configured'); | |||
} | |||
} | |||
function getMode( | |||
isBitbucketOption: boolean, | |||
hasBitbucketCloudConf: boolean, | |||
mode: CreateProjectModes | |||
) { | |||
return isBitbucketOption && hasBitbucketCloudConf ? CreateProjectModes.BitbucketCloud : mode; | |||
} | |||
function renderAlmOption( | |||
props: CreateProjectModeSelectionProps, | |||
alm: AlmKeys.Azure | AlmKeys.BitbucketServer | AlmKeys.GitHub | AlmKeys.GitLab, | |||
@@ -61,7 +88,7 @@ function renderAlmOption( | |||
? almCounts[AlmKeys.BitbucketServer] + almCounts[AlmKeys.BitbucketCloud] | |||
: almCounts[alm]; | |||
const hasConfig = count > 0; | |||
const hasTooManyConfig = count > 1; | |||
const hasTooManyConfig = count > 1 && !ALLOWED_MULTIPLE_CONFIGS.includes(alm); | |||
const disabled = loadingBindings || hasTooManyConfig || (!hasConfig && !canAdmin); | |||
const onClick = () => { | |||
@@ -73,23 +100,10 @@ function renderAlmOption( | |||
return props.onConfigMode(alm); | |||
} | |||
return props.onSelectMode( | |||
isBitbucketOption && hasBitbucketCloudConf ? CreateProjectModes.BitbucketCloud : mode | |||
); | |||
return props.onSelectMode(getMode(isBitbucketOption, hasBitbucketCloudConf, mode)); | |||
}; | |||
let errorMessage = ''; | |||
if (hasTooManyConfig) { | |||
errorMessage = translateWithParameters( | |||
'onboarding.create_project.too_many_alm_instances_X', | |||
translate('alm', alm) | |||
); | |||
} else if (!hasConfig) { | |||
errorMessage = canAdmin | |||
? translate('onboarding.create_project.alm_not_configured.admin') | |||
: translate('onboarding.create_project.alm_not_configured'); | |||
} | |||
const errorMessage = getErrorMessage(hasTooManyConfig, hasConfig, canAdmin, alm); | |||
return ( | |||
<div className="display-flex-column"> |
@@ -219,7 +219,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> { | |||
location={location} | |||
onProjectCreate={this.handleProjectCreate} | |||
router={router} | |||
settings={gitlabSettings} | |||
almInstances={gitlabSettings} | |||
/> | |||
); | |||
} |
@@ -29,7 +29,7 @@ interface Props { | |||
canAdmin: boolean; | |||
loadingBindings: boolean; | |||
onProjectCreate: (projectKey: string) => void; | |||
settings: AlmSettingsInstance[]; | |||
almInstances: AlmSettingsInstance[]; | |||
location: Location; | |||
router: Router; | |||
} | |||
@@ -43,7 +43,7 @@ interface State { | |||
resetPat: boolean; | |||
searching: boolean; | |||
searchQuery: string; | |||
settings?: AlmSettingsInstance; | |||
selectedAlmInstance: AlmSettingsInstance; | |||
showPersonalAccessTokenForm: boolean; | |||
} | |||
@@ -63,7 +63,7 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
showPersonalAccessTokenForm: true, | |||
searching: false, | |||
searchQuery: '', | |||
settings: props.settings.length === 1 ? props.settings[0] : undefined, | |||
selectedAlmInstance: props.almInstances[0], | |||
}; | |||
} | |||
@@ -72,11 +72,9 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.settings.length === 0 && this.props.settings.length > 0) { | |||
this.setState( | |||
{ settings: this.props.settings.length === 1 ? this.props.settings[0] : undefined }, | |||
() => this.fetchInitialData() | |||
); | |||
const { almInstances } = this.props; | |||
if (prevProps.almInstances.length === 0 && this.props.almInstances.length > 0) { | |||
this.setState({ selectedAlmInstance: almInstances[0] }, () => this.fetchInitialData()); | |||
} | |||
} | |||
@@ -115,14 +113,14 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
}; | |||
fetchProjects = async (pageIndex = 1, query?: string) => { | |||
const { settings } = this.state; | |||
if (!settings) { | |||
const { selectedAlmInstance } = this.state; | |||
if (!selectedAlmInstance) { | |||
return Promise.resolve(undefined); | |||
} | |||
try { | |||
return await getGitlabProjects({ | |||
almSetting: settings.key, | |||
almSetting: selectedAlmInstance.key, | |||
page: pageIndex, | |||
pageSize: GITLAB_PROJECTS_PAGESIZE, | |||
query, | |||
@@ -133,15 +131,15 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
}; | |||
doImport = async (gitlabProjectId: string) => { | |||
const { settings } = this.state; | |||
const { selectedAlmInstance } = this.state; | |||
if (!settings) { | |||
if (!selectedAlmInstance) { | |||
return Promise.resolve(undefined); | |||
} | |||
try { | |||
return await importGitlabProject({ | |||
almSetting: settings.key, | |||
almSetting: selectedAlmInstance.key, | |||
gitlabProjectId, | |||
}); | |||
} catch (_) { | |||
@@ -172,7 +170,6 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
} = this.state; | |||
const result = await this.fetchProjects(pageIndex + 1, searchQuery); | |||
if (this.mounted) { | |||
this.setState(({ projects = [], projectsPaging }) => ({ | |||
loadingMore: false, | |||
@@ -186,7 +183,6 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
this.setState({ searching: true, searchQuery }); | |||
const result = await this.fetchProjects(1, searchQuery); | |||
if (this.mounted) { | |||
this.setState(({ projects, projectsPaging }) => ({ | |||
searching: false, | |||
@@ -208,8 +204,17 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
await this.fetchInitialData(); | |||
}; | |||
onChangeConfig = (instance: AlmSettingsInstance) => { | |||
this.setState({ | |||
selectedAlmInstance: instance, | |||
showPersonalAccessTokenForm: true, | |||
projects: undefined, | |||
resetPat: false, | |||
}); | |||
}; | |||
render() { | |||
const { canAdmin, loadingBindings, location } = this.props; | |||
const { loadingBindings, location, almInstances, canAdmin } = this.props; | |||
const { | |||
importingGitlabProjectId, | |||
loading, | |||
@@ -219,14 +224,15 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
resetPat, | |||
searching, | |||
searchQuery, | |||
settings, | |||
selectedAlmInstance, | |||
showPersonalAccessTokenForm, | |||
} = this.state; | |||
return ( | |||
<GitlabProjectCreateRenderer | |||
settings={settings} | |||
canAdmin={canAdmin} | |||
almInstances={almInstances} | |||
selectedAlmInstance={selectedAlmInstance} | |||
importingGitlabProjectId={importingGitlabProjectId} | |||
loading={loading || loadingBindings} | |||
loadingMore={loadingMore} | |||
@@ -242,6 +248,7 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
showPersonalAccessTokenForm={ | |||
showPersonalAccessTokenForm || Boolean(location.query.resetPat) | |||
} | |||
onChangeConfig={this.onChangeConfig} | |||
/> | |||
); | |||
} |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import AlmSettingsInstanceSelector from '../../../components/devops-platform/AlmSettingsInstanceSelector'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/system'; | |||
import { GitlabProject } from '../../../types/alm-integration'; | |||
@@ -42,8 +43,10 @@ export interface GitlabProjectCreateRendererProps { | |||
resetPat: boolean; | |||
searching: boolean; | |||
searchQuery: string; | |||
settings?: AlmSettingsInstance; | |||
almInstances?: AlmSettingsInstance[]; | |||
selectedAlmInstance?: AlmSettingsInstance; | |||
showPersonalAccessTokenForm?: boolean; | |||
onChangeConfig: (instance: AlmSettingsInstance) => void; | |||
} | |||
export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) { | |||
@@ -57,7 +60,8 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe | |||
resetPat, | |||
searching, | |||
searchQuery, | |||
settings, | |||
selectedAlmInstance, | |||
almInstances, | |||
showPersonalAccessTokenForm, | |||
} = props; | |||
@@ -77,17 +81,32 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe | |||
} | |||
/> | |||
{almInstances && almInstances.length > 1 && ( | |||
<div className="display-flex-column huge-spacer-bottom"> | |||
<label htmlFor="alm-config-selector" className="spacer-bottom"> | |||
{translate('alm.configuration.selector.label')} | |||
</label> | |||
<AlmSettingsInstanceSelector | |||
instances={almInstances} | |||
onChange={props.onChangeConfig} | |||
initialValue={selectedAlmInstance ? selectedAlmInstance.key : undefined} | |||
classNames="abs-width-400" | |||
inputId="alm-config-selector" | |||
/> | |||
</div> | |||
)} | |||
{loading && <i className="spinner" />} | |||
{!loading && !settings && ( | |||
{!loading && !selectedAlmInstance && ( | |||
<WrongBindingCountAlert alm={AlmKeys.GitLab} canAdmin={!!canAdmin} /> | |||
)} | |||
{!loading && | |||
settings && | |||
selectedAlmInstance && | |||
(showPersonalAccessTokenForm ? ( | |||
<PersonalAccessTokenForm | |||
almSetting={settings} | |||
almSetting={selectedAlmInstance} | |||
resetPat={resetPat} | |||
onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated} | |||
/> |
@@ -75,12 +75,26 @@ export default class PersonalAccessTokenForm extends React.PureComponent<Props, | |||
}; | |||
} | |||
async componentDidMount() { | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.checkPATAndUpdateView(); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (this.props.almSetting !== prevProps.almSetting) { | |||
this.checkPATAndUpdateView(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
checkPATAndUpdateView = async () => { | |||
const { | |||
almSetting: { key }, | |||
resetPat, | |||
} = this.props; | |||
this.mounted = true; | |||
// We don't need to check PAT if we want to reset | |||
if (!resetPat) { | |||
@@ -106,11 +120,7 @@ export default class PersonalAccessTokenForm extends React.PureComponent<Props, | |||
} | |||
} | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
}; | |||
handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |||
this.setState({ | |||
@@ -379,7 +389,7 @@ export default class PersonalAccessTokenForm extends React.PureComponent<Props, | |||
className={classNames('input-super-large', { | |||
'is-invalid': isInvalid, | |||
})} | |||
id="personal_access_token" | |||
id="personal_access_token_validation" | |||
minLength={1} | |||
value={password} | |||
onChange={this.handlePasswordChange} |
@@ -0,0 +1,105 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { act, screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import selectEvent from 'react-select-event'; | |||
import { byLabelText, byRole, byText } from 'testing-library-selector'; | |||
import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock'; | |||
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock'; | |||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||
import CreateProjectPage from '../CreateProjectPage'; | |||
jest.mock('../../../../api/alm-integrations'); | |||
jest.mock('../../../../api/alm-settings'); | |||
let almIntegrationHandler: AlmIntegrationsServiceMock; | |||
let almSettingsHandler: AlmSettingsServiceMock; | |||
const ui = { | |||
gitlabCreateProjectButton: byText('onboarding.create_project.select_method.gitlab'), | |||
personalAccessTokenInput: byRole('textbox', { | |||
name: 'onboarding.create_project.enter_pat field_required', | |||
}), | |||
instanceSelector: byLabelText('alm.configuration.selector.label'), | |||
}; | |||
beforeAll(() => { | |||
almIntegrationHandler = new AlmIntegrationsServiceMock(); | |||
almSettingsHandler = new AlmSettingsServiceMock(); | |||
}); | |||
afterEach(() => { | |||
almIntegrationHandler.reset(); | |||
almSettingsHandler.reset(); | |||
}); | |||
describe('Gitlab onboarding page', () => { | |||
it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => { | |||
const user = userEvent.setup(); | |||
renderCreateProject(); | |||
expect(ui.gitlabCreateProjectButton.get()).toBeInTheDocument(); | |||
await user.click(ui.gitlabCreateProjectButton.get()); | |||
expect(screen.getByText('onboarding.create_project.gitlab.title')).toBeInTheDocument(); | |||
expect(screen.getByText('alm.configuration.selector.label')).toBeInTheDocument(); | |||
expect(screen.getByText('onboarding.create_project.enter_pat')).toBeInTheDocument(); | |||
expect(screen.getByText('onboarding.create_project.pat_help.title')).toBeInTheDocument(); | |||
expect(screen.getByRole('button', { name: 'save' })).toBeInTheDocument(); | |||
await user.click(ui.personalAccessTokenInput.get()); | |||
await user.keyboard('secret'); | |||
await user.click(screen.getByRole('button', { name: 'save' })); | |||
expect(screen.getByText('Gitlab project 1')).toBeInTheDocument(); | |||
expect(screen.getByText('Gitlab project 2')).toBeInTheDocument(); | |||
expect(screen.getAllByText('onboarding.create_project.set_up')).toHaveLength(2); | |||
expect(screen.getByText('onboarding.create_project.repository_imported')).toBeInTheDocument(); | |||
}); | |||
it('should show import project feature when PAT is already set', async () => { | |||
const user = userEvent.setup(); | |||
renderCreateProject(); | |||
await act(async () => { | |||
await user.click(ui.gitlabCreateProjectButton.get()); | |||
await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]); | |||
}); | |||
expect(screen.getByText('Gitlab project 1')).toBeInTheDocument(); | |||
expect(screen.getByText('Gitlab project 2')).toBeInTheDocument(); | |||
}); | |||
it('should show no result message when there are no projects', async () => { | |||
const user = userEvent.setup(); | |||
almIntegrationHandler.setGitlabProjects([]); | |||
renderCreateProject(); | |||
await act(async () => { | |||
await user.click(ui.gitlabCreateProjectButton.get()); | |||
await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]); | |||
}); | |||
expect(screen.getByText('onboarding.create_project.gitlab.no_projects')).toBeInTheDocument(); | |||
}); | |||
}); | |||
function renderCreateProject() { | |||
renderApp('project/create', <CreateProjectPage />); | |||
} |
@@ -154,7 +154,7 @@ it('should import', async () => { | |||
}); | |||
it('should do nothing with missing settings', async () => { | |||
const wrapper = shallowRender({ settings: [] }); | |||
const wrapper = shallowRender({ almInstances: [] }); | |||
await wrapper.instance().handleLoadMore(); | |||
await wrapper.instance().handleSearch('whatever'); | |||
@@ -204,7 +204,7 @@ function shallowRender(props: Partial<GitlabProjectCreate['props']> = {}) { | |||
location={mockLocation()} | |||
onProjectCreate={jest.fn()} | |||
router={mockRouter()} | |||
settings={[mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: almSettingKey })]} | |||
almInstances={[mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: almSettingKey })]} | |||
{...props} | |||
/> | |||
); |
@@ -27,8 +27,8 @@ import GitlabProjectCreateRenderer, { | |||
it('should render correctly', () => { | |||
expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); | |||
expect(shallowRender({ settings: undefined })).toMatchSnapshot('invalid settings'); | |||
expect(shallowRender({ canAdmin: true, settings: undefined })).toMatchSnapshot( | |||
expect(shallowRender({ almInstances: undefined })).toMatchSnapshot('invalid settings'); | |||
expect(shallowRender({ almInstances: undefined })).toMatchSnapshot( | |||
'invalid settings, admin user' | |||
); | |||
expect(shallowRender()).toMatchSnapshot('pat form'); | |||
@@ -47,13 +47,19 @@ function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) { | |||
onLoadMore={jest.fn()} | |||
onPersonalAccessTokenCreated={jest.fn()} | |||
onSearch={jest.fn()} | |||
onChangeConfig={jest.fn()} | |||
projects={undefined} | |||
projectsPaging={{ pageIndex: 1, pageSize: 30, total: 0 }} | |||
searching={false} | |||
searchQuery="" | |||
resetPat={false} | |||
showPersonalAccessTokenForm={true} | |||
settings={mockAlmSettingsInstance({ alm: AlmKeys.GitLab })} | |||
almInstances={[ | |||
mockAlmSettingsInstance({ alm: AlmKeys.GitLab }), | |||
mockAlmSettingsInstance({ alm: AlmKeys.GitLab }), | |||
mockAlmSettingsInstance({ alm: AlmKeys.GitHub }), | |||
]} | |||
selectedAlmInstance={mockAlmSettingsInstance({ alm: AlmKeys.GitLab })} | |||
{...props} | |||
/> | |||
); |
@@ -97,7 +97,7 @@ it('should correctly handle form for bitbucket interactions', async () => { | |||
// Submit button disabled by default. | |||
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true); | |||
change(wrapper.find('#personal_access_token'), 'token'); | |||
change(wrapper.find('input#personal_access_token_validation'), 'token'); | |||
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true); | |||
// Submit button enabled if there's a value. | |||
@@ -120,7 +120,7 @@ it('should show error when issue', async () => { | |||
(checkPersonalAccessTokenIsValid as jest.Mock).mockRejectedValueOnce({}); | |||
change(wrapper.find('#personal_access_token'), 'token'); | |||
change(wrapper.find('input#personal_access_token_validation'), 'token'); | |||
change(wrapper.find('#username'), 'username'); | |||
// Expect correct calls to be made when submitting. |
@@ -296,6 +296,7 @@ exports[`should render correctly for gitlab mode 1`] = ` | |||
id="create-project" | |||
> | |||
<GitlabProjectCreate | |||
almInstances={Array []} | |||
canAdmin={false} | |||
loadingBindings={true} | |||
location={ | |||
@@ -324,7 +325,6 @@ exports[`should render correctly for gitlab mode 1`] = ` | |||
"setRouteLeaveHook": [MockFunction], | |||
} | |||
} | |||
settings={Array []} | |||
/> | |||
</div> | |||
</Fragment> |
@@ -2,9 +2,18 @@ | |||
exports[`should render correctly 1`] = ` | |||
<GitlabProjectCreateRenderer | |||
almInstances={ | |||
Array [ | |||
Object { | |||
"alm": "gitlab", | |||
"key": "gitlab-setting", | |||
}, | |||
] | |||
} | |||
canAdmin={false} | |||
loading={false} | |||
loadingMore={false} | |||
onChangeConfig={[Function]} | |||
onImport={[Function]} | |||
onLoadMore={[Function]} | |||
onPersonalAccessTokenCreated={[Function]} | |||
@@ -19,7 +28,7 @@ exports[`should render correctly 1`] = ` | |||
resetPat={false} | |||
searchQuery="" | |||
searching={false} | |||
settings={ | |||
selectedAlmInstance={ | |||
Object { | |||
"alm": "gitlab", | |||
"key": "gitlab-setting", |
@@ -17,9 +17,15 @@ exports[`should render correctly: invalid settings 1`] = ` | |||
</span> | |||
} | |||
/> | |||
<WrongBindingCountAlert | |||
alm="gitlab" | |||
canAdmin={false} | |||
<PersonalAccessTokenForm | |||
almSetting={ | |||
Object { | |||
"alm": "gitlab", | |||
"key": "key", | |||
} | |||
} | |||
onPersonalAccessTokenCreated={[MockFunction]} | |||
resetPat={false} | |||
/> | |||
</Fragment> | |||
`; | |||
@@ -41,9 +47,15 @@ exports[`should render correctly: invalid settings, admin user 1`] = ` | |||
</span> | |||
} | |||
/> | |||
<WrongBindingCountAlert | |||
alm="gitlab" | |||
canAdmin={true} | |||
<PersonalAccessTokenForm | |||
almSetting={ | |||
Object { | |||
"alm": "gitlab", | |||
"key": "key", | |||
} | |||
} | |||
onPersonalAccessTokenCreated={[MockFunction]} | |||
resetPat={false} | |||
/> | |||
</Fragment> | |||
`; | |||
@@ -65,6 +77,38 @@ exports[`should render correctly: loading 1`] = ` | |||
</span> | |||
} | |||
/> | |||
<div | |||
className="display-flex-column huge-spacer-bottom" | |||
> | |||
<label | |||
className="spacer-bottom" | |||
htmlFor="alm-config-selector" | |||
> | |||
alm.configuration.selector.label | |||
</label> | |||
<AlmSettingsInstanceSelector | |||
classNames="abs-width-400" | |||
initialValue="key" | |||
inputId="alm-config-selector" | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "gitlab", | |||
"key": "key", | |||
}, | |||
Object { | |||
"alm": "gitlab", | |||
"key": "key", | |||
}, | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
}, | |||
] | |||
} | |||
onChange={[MockFunction]} | |||
/> | |||
</div> | |||
<i | |||
className="spinner" | |||
/> | |||
@@ -88,6 +132,38 @@ exports[`should render correctly: pat form 1`] = ` | |||
</span> | |||
} | |||
/> | |||
<div | |||
className="display-flex-column huge-spacer-bottom" | |||
> | |||
<label | |||
className="spacer-bottom" | |||
htmlFor="alm-config-selector" | |||
> | |||
alm.configuration.selector.label | |||
</label> | |||
<AlmSettingsInstanceSelector | |||
classNames="abs-width-400" | |||
initialValue="key" | |||
inputId="alm-config-selector" | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "gitlab", | |||
"key": "key", | |||
}, | |||
Object { | |||
"alm": "gitlab", | |||
"key": "key", | |||
}, | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
}, | |||
] | |||
} | |||
onChange={[MockFunction]} | |||
/> | |||
</div> | |||
<PersonalAccessTokenForm | |||
almSetting={ | |||
Object { | |||
@@ -118,6 +194,38 @@ exports[`should render correctly: project selection form 1`] = ` | |||
</span> | |||
} | |||
/> | |||
<div | |||
className="display-flex-column huge-spacer-bottom" | |||
> | |||
<label | |||
className="spacer-bottom" | |||
htmlFor="alm-config-selector" | |||
> | |||
alm.configuration.selector.label | |||
</label> | |||
<AlmSettingsInstanceSelector | |||
classNames="abs-width-400" | |||
initialValue="key" | |||
inputId="alm-config-selector" | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "gitlab", | |||
"key": "key", | |||
}, | |||
Object { | |||
"alm": "gitlab", | |||
"key": "key", | |||
}, | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
}, | |||
] | |||
} | |||
onChange={[MockFunction]} | |||
/> | |||
</div> | |||
<GitlabProjectSelectionForm | |||
loadingMore={false} | |||
onImport={[MockFunction]} |
@@ -29,7 +29,7 @@ exports[`should render correctly: bitbucket 1`] = ` | |||
<input | |||
autoFocus={true} | |||
className="input-super-large is-invalid" | |||
id="personal_access_token" | |||
id="personal_access_token_validation" | |||
minLength={1} | |||
onChange={[Function]} | |||
type="text" | |||
@@ -180,7 +180,7 @@ exports[`should render correctly: bitbucket cloud 1`] = ` | |||
<input | |||
autoFocus={false} | |||
className="input-super-large is-invalid" | |||
id="personal_access_token" | |||
id="personal_access_token_validation" | |||
minLength={1} | |||
onChange={[Function]} | |||
type="text" | |||
@@ -321,7 +321,7 @@ exports[`should render correctly: gitlab 1`] = ` | |||
<input | |||
autoFocus={true} | |||
className="input-super-large is-invalid" | |||
id="personal_access_token" | |||
id="personal_access_token_validation" | |||
minLength={1} | |||
onChange={[Function]} | |||
type="text" | |||
@@ -431,7 +431,7 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = ` | |||
<input | |||
autoFocus={true} | |||
className="input-super-large is-invalid" | |||
id="personal_access_token" | |||
id="personal_access_token_validation" | |||
minLength={1} | |||
onChange={[Function]} | |||
type="text" | |||
@@ -566,7 +566,7 @@ exports[`should show error when issue: issue submitting token 1`] = ` | |||
<input | |||
autoFocus={false} | |||
className="input-super-large is-invalid" | |||
id="personal_access_token" | |||
id="personal_access_token_validation" | |||
minLength={1} | |||
onChange={[Function]} | |||
type="text" |
@@ -1,3 +1,5 @@ | |||
import { AlmKeys } from '../../../types/alm-settings'; | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
@@ -20,3 +22,5 @@ | |||
export const PROJECT_NAME_MAX_LEN = 255; | |||
export const DEFAULT_BBS_PAGE_SIZE = 25; | |||
export const ALLOWED_MULTIPLE_CONFIGS = [AlmKeys.GitLab]; |
@@ -31,6 +31,7 @@ import { hasGlobalPermission } from '../../../helpers/users'; | |||
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; | |||
import { Permissions } from '../../../types/permissions'; | |||
import { LoggedInUser } from '../../../types/users'; | |||
import { ALLOWED_MULTIPLE_CONFIGS } from '../../create/project/constants'; | |||
import ProjectCreationMenuItem from './ProjectCreationMenuItem'; | |||
interface Props { | |||
@@ -90,7 +91,7 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> { | |||
currentAlmSettings = almSettings.filter((s) => s.alm === key); | |||
} | |||
return ( | |||
currentAlmSettings.length === 1 && | |||
this.configLengthChecker(key, currentAlmSettings.length) && | |||
key === currentAlmSettings[0].alm && | |||
this.almSettingIsValid(currentAlmSettings[0]) | |||
); | |||
@@ -103,6 +104,10 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> { | |||
} | |||
}; | |||
configLengthChecker = (key: AlmKeys, length: number) => { | |||
return ALLOWED_MULTIPLE_CONFIGS.includes(key) ? length > 0 : length === 1; | |||
}; | |||
render() { | |||
const { className, currentUser } = this.props; | |||
const { boundAlms } = this.state; |
@@ -121,7 +121,7 @@ it('should filter alm bindings appropriately', async () => { | |||
wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().boundAlms).toEqual([]); | |||
expect(wrapper.state().boundAlms).toEqual([AlmKeys.GitLab]); | |||
}); | |||
function shallowRender(overrides: Partial<ProjectCreationMenu['props']> = {}) { |
@@ -38,6 +38,7 @@ import { | |||
AlmSettingsBindingStatusType, | |||
} from '../../../../types/alm-settings'; | |||
import { EditionKey } from '../../../../types/editions'; | |||
import { ALLOWED_MULTIPLE_CONFIGS } from '../../../create/project/constants'; | |||
export interface AlmBindingDefinitionBoxProps { | |||
alm: AlmKeys; | |||
@@ -110,7 +111,7 @@ function getImportFeatureStatus( | |||
multipleDefinitions: boolean, | |||
type: AlmSettingsBindingStatusType.Success | AlmSettingsBindingStatusType.Failure | |||
) { | |||
if (multipleDefinitions) { | |||
if (multipleDefinitions && !ALLOWED_MULTIPLE_CONFIGS.includes(alm)) { | |||
return ( | |||
<div className="display-inline-flex-center"> | |||
<strong className="spacer-left"> |
@@ -19,10 +19,9 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { components, OptionProps, SingleValueProps } from 'react-select'; | |||
import Link from '../../../../components/common/Link'; | |||
import { Button, SubmitButton } from '../../../../components/controls/buttons'; | |||
import Select from '../../../../components/controls/Select'; | |||
import AlmSettingsInstanceSelector from '../../../../components/devops-platform/AlmSettingsInstanceSelector'; | |||
import AlertSuccessIcon from '../../../../components/icons/AlertSuccessIcon'; | |||
import { Alert } from '../../../../components/ui/Alert'; | |||
import DeferredSpinner from '../../../../components/ui/DeferredSpinner'; | |||
@@ -57,25 +56,6 @@ export interface PRDecorationBindingRendererProps { | |||
isSysAdmin: boolean; | |||
} | |||
function optionRenderer(props: OptionProps<AlmSettingsInstance, false>) { | |||
return <components.Option {...props}>{customOptions(props.data)}</components.Option>; | |||
} | |||
function singleValueRenderer(props: SingleValueProps<AlmSettingsInstance>) { | |||
return <components.SingleValue {...props}>{customOptions(props.data)}</components.SingleValue>; | |||
} | |||
function customOptions(instance: AlmSettingsInstance) { | |||
return instance.url ? ( | |||
<> | |||
<span>{instance.key} — </span> | |||
<span className="text-muted">{instance.url}</span> | |||
</> | |||
) : ( | |||
<span>{instance.key}</span> | |||
); | |||
} | |||
export default function PRDecorationBindingRenderer(props: PRDecorationBindingRendererProps) { | |||
const { | |||
formData, | |||
@@ -151,18 +131,12 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe | |||
</div> | |||
</div> | |||
<div className="settings-definition-right"> | |||
<Select | |||
inputId="name" | |||
className="abs-width-400 big-spacer-top it__configuration-name-select" | |||
isClearable={false} | |||
isSearchable={false} | |||
options={instances} | |||
<AlmSettingsInstanceSelector | |||
instances={instances} | |||
onChange={(instance: AlmSettingsInstance) => props.onFieldChange('key', instance.key)} | |||
components={{ | |||
Option: optionRenderer, | |||
SingleValue: singleValueRenderer, | |||
}} | |||
value={instances.filter((instance) => instance.key === formData.key)} | |||
initialValue={formData.key} | |||
classNames="abs-width-400 big-spacer-top it__configuration-name-select" | |||
inputId="name" | |||
/> | |||
</div> | |||
</div> |
@@ -129,19 +129,11 @@ exports[`should render correctly: when there are configuration errors (admin use | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
className="abs-width-400 big-spacer-top it__configuration-name-select" | |||
components={ | |||
Object { | |||
"Option": [Function], | |||
"SingleValue": [Function], | |||
} | |||
} | |||
<AlmSettingsInstanceSelector | |||
classNames="abs-width-400 big-spacer-top it__configuration-name-select" | |||
initialValue="i1" | |||
inputId="name" | |||
isClearable={false} | |||
isSearchable={false} | |||
onChange={[Function]} | |||
options={ | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
@@ -164,15 +156,7 @@ exports[`should render correctly: when there are configuration errors (admin use | |||
}, | |||
] | |||
} | |||
value={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "i1", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
] | |||
} | |||
onChange={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
@@ -329,19 +313,11 @@ exports[`should render correctly: when there are configuration errors (admin use | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
className="abs-width-400 big-spacer-top it__configuration-name-select" | |||
components={ | |||
Object { | |||
"Option": [Function], | |||
"SingleValue": [Function], | |||
} | |||
} | |||
<AlmSettingsInstanceSelector | |||
classNames="abs-width-400 big-spacer-top it__configuration-name-select" | |||
initialValue="" | |||
inputId="name" | |||
isClearable={false} | |||
isSearchable={false} | |||
onChange={[Function]} | |||
options={ | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
@@ -364,7 +340,7 @@ exports[`should render correctly: when there are configuration errors (admin use | |||
}, | |||
] | |||
} | |||
value={Array []} | |||
onChange={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
@@ -467,19 +443,11 @@ exports[`should render correctly: when there are configuration errors (non-admin | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
className="abs-width-400 big-spacer-top it__configuration-name-select" | |||
components={ | |||
Object { | |||
"Option": [Function], | |||
"SingleValue": [Function], | |||
} | |||
} | |||
<AlmSettingsInstanceSelector | |||
classNames="abs-width-400 big-spacer-top it__configuration-name-select" | |||
initialValue="" | |||
inputId="name" | |||
isClearable={false} | |||
isSearchable={false} | |||
onChange={[Function]} | |||
options={ | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
@@ -502,7 +470,7 @@ exports[`should render correctly: when there are configuration errors (non-admin | |||
}, | |||
] | |||
} | |||
value={Array []} | |||
onChange={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
@@ -608,19 +576,11 @@ exports[`should render correctly: with a single ALM instance 1`] = ` | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
className="abs-width-400 big-spacer-top it__configuration-name-select" | |||
components={ | |||
Object { | |||
"Option": [Function], | |||
"SingleValue": [Function], | |||
} | |||
} | |||
<AlmSettingsInstanceSelector | |||
classNames="abs-width-400 big-spacer-top it__configuration-name-select" | |||
initialValue="" | |||
inputId="name" | |||
isClearable={false} | |||
isSearchable={false} | |||
onChange={[Function]} | |||
options={ | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
@@ -629,7 +589,7 @@ exports[`should render correctly: with a single ALM instance 1`] = ` | |||
}, | |||
] | |||
} | |||
value={Array []} | |||
onChange={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
@@ -686,19 +646,11 @@ exports[`should render correctly: with a valid and saved form 1`] = ` | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
className="abs-width-400 big-spacer-top it__configuration-name-select" | |||
components={ | |||
Object { | |||
"Option": [Function], | |||
"SingleValue": [Function], | |||
} | |||
} | |||
<AlmSettingsInstanceSelector | |||
classNames="abs-width-400 big-spacer-top it__configuration-name-select" | |||
initialValue="i1" | |||
inputId="name" | |||
isClearable={false} | |||
isSearchable={false} | |||
onChange={[Function]} | |||
options={ | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
@@ -721,15 +673,7 @@ exports[`should render correctly: with a valid and saved form 1`] = ` | |||
}, | |||
] | |||
} | |||
value={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "i1", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
] | |||
} | |||
onChange={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
@@ -848,19 +792,11 @@ exports[`should render correctly: with an empty form 1`] = ` | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
className="abs-width-400 big-spacer-top it__configuration-name-select" | |||
components={ | |||
Object { | |||
"Option": [Function], | |||
"SingleValue": [Function], | |||
} | |||
} | |||
<AlmSettingsInstanceSelector | |||
classNames="abs-width-400 big-spacer-top it__configuration-name-select" | |||
initialValue="" | |||
inputId="name" | |||
isClearable={false} | |||
isSearchable={false} | |||
onChange={[Function]} | |||
options={ | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
@@ -883,7 +819,7 @@ exports[`should render correctly: with an empty form 1`] = ` | |||
}, | |||
] | |||
} | |||
value={Array []} | |||
onChange={[Function]} | |||
/> | |||
</div> | |||
</div> |
@@ -0,0 +1,75 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { components, OptionProps, SingleValueProps } from 'react-select'; | |||
import { AlmSettingsInstance } from '../../types/alm-settings'; | |||
import Select from '../controls/Select'; | |||
function optionRenderer(props: OptionProps<AlmSettingsInstance, false>) { | |||
return <components.Option {...props}>{customOptions(props.data)}</components.Option>; | |||
} | |||
function singleValueRenderer(props: SingleValueProps<AlmSettingsInstance>) { | |||
return <components.SingleValue {...props}>{customOptions(props.data)}</components.SingleValue>; | |||
} | |||
function customOptions(instance: AlmSettingsInstance) { | |||
return instance.url ? ( | |||
<> | |||
<span>{instance.key} — </span> | |||
<span className="text-muted">{instance.url}</span> | |||
</> | |||
) : ( | |||
<span>{instance.key}</span> | |||
); | |||
} | |||
interface Props { | |||
instances: AlmSettingsInstance[]; | |||
initialValue?: string; | |||
onChange: (instance: AlmSettingsInstance) => void; | |||
classNames: string; | |||
inputId: string; | |||
} | |||
export default function AlmSettingsInstanceSelector(props: Props) { | |||
const { instances, initialValue, classNames, inputId } = props; | |||
return ( | |||
<Select | |||
inputId={inputId} | |||
className={classNames} | |||
isClearable={false} | |||
isSearchable={false} | |||
options={instances} | |||
onChange={(inst) => { | |||
if (inst) { | |||
props.onChange(inst); | |||
} | |||
}} | |||
components={{ | |||
Option: optionRenderer, | |||
SingleValue: singleValueRenderer, | |||
}} | |||
getOptionValue={(opt) => opt.key} | |||
value={instances.find((inst) => inst.key === initialValue)} | |||
/> | |||
); | |||
} |
@@ -389,6 +389,7 @@ alm.github=GitHub | |||
alm.github.short=GitHub | |||
alm.gitlab=GitLab | |||
alm.gitlab.short=GitLab | |||
alm.configuration.selector.label=What DevOps platform do you want to import project from? | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -3598,7 +3599,7 @@ onboarding.create_project.github.warning.message_admin=Please make sure the GitH | |||
onboarding.create_project.github.warning.message_admin.link=DevOps Platform integration settings | |||
onboarding.create_project.github.no_orgs=We couldn't load any organizations with your key. Contact an administrator. | |||
onboarding.create_project.github.no_orgs_admin=We couldn't load any organizations. Make sure the GitHub App is installed in at least one organization and check the GitHub instance configuration in the {link}. | |||
onboarding.create_project.gitlab.title=Which GitLab project do you want to set up? | |||
onboarding.create_project.gitlab.title=Gitlab project onboarding | |||
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 | |||