@@ -17,7 +17,14 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { get, getJSON, HttpStatus, parseError, post } from 'sonar-ui-common/helpers/request'; | |||
import { | |||
get, | |||
getJSON, | |||
HttpStatus, | |||
parseError, | |||
parseJSON, | |||
post | |||
} from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { | |||
AlmSettingsBindingDefinitions, | |||
@@ -32,6 +39,7 @@ import { | |||
GithubProjectAlmBindingParams, | |||
GitlabBindingDefinition, | |||
GitlabProjectAlmBindingParams, | |||
ProjectAlmBindingConfigurationErrors, | |||
ProjectAlmBindingResponse | |||
} from '../types/alm-settings'; | |||
@@ -142,3 +150,16 @@ export function setProjectGithubBinding(data: GithubProjectAlmBindingParams) { | |||
export function setProjectGitlabBinding(data: GitlabProjectAlmBindingParams) { | |||
return post('/api/alm_settings/set_gitlab_binding', data).catch(throwGlobalError); | |||
} | |||
export function validateProjectAlmBinding( | |||
projectKey: string | |||
): Promise<ProjectAlmBindingConfigurationErrors | undefined> { | |||
return get('/api/alm_settings/validate_binding', { project: projectKey }) | |||
.then(() => undefined) | |||
.catch((response: Response) => { | |||
if (response.status === HttpStatus.BadRequest) { | |||
return parseJSON(response); | |||
} | |||
return throwGlobalError(response); | |||
}); | |||
} |
@@ -87,7 +87,7 @@ exports[`should render additional categories component correctly 4`] = ` | |||
`; | |||
exports[`should render additional categories component correctly 5`] = ` | |||
<Connect(PRDecorationBinding) | |||
<Connect(Connect(withCurrentUser(PRDecorationBinding))) | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], |
@@ -58,8 +58,14 @@ export class AlmIntegration extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
let currentAlm = props.location.query.alm || AlmKeys.GitHub; | |||
if (currentAlm === AlmKeys.BitbucketCloud) { | |||
currentAlm = AlmKeys.BitbucketServer; | |||
} | |||
this.state = { | |||
currentAlm: props.location.query.alm || AlmKeys.GitHub, | |||
currentAlm, | |||
definitions: { | |||
[AlmKeys.Azure]: [], | |||
[AlmKeys.BitbucketServer]: [], |
@@ -162,8 +162,16 @@ it('should fetch settings', async () => { | |||
expect(wrapper.state().loadingAlmDefinitions).toBe(false); | |||
}); | |||
function shallowRender() { | |||
it('should detect the current ALM from the query', () => { | |||
let wrapper = shallowRender({ location: mockLocation() }); | |||
expect(wrapper.state().currentAlm).toBe(AlmKeys.GitHub); | |||
wrapper = shallowRender({ location: mockLocation({ query: { alm: AlmKeys.BitbucketCloud } }) }); | |||
expect(wrapper.state().currentAlm).toBe(AlmKeys.BitbucketServer); | |||
}); | |||
function shallowRender(props: Partial<AlmIntegration['props']> = {}) { | |||
return shallow<AlmIntegration>( | |||
<AlmIntegration appState={{ branchesEnabled: true }} location={mockLocation()} /> | |||
<AlmIntegration appState={{ branchesEnabled: true }} location={mockLocation()} {...props} /> | |||
); | |||
} |
@@ -28,16 +28,21 @@ import { | |||
setProjectBitbucketBinding, | |||
setProjectBitbucketCloudBinding, | |||
setProjectGithubBinding, | |||
setProjectGitlabBinding | |||
setProjectGitlabBinding, | |||
validateProjectAlmBinding | |||
} from '../../../../api/alm-settings'; | |||
import throwGlobalError from '../../../../app/utils/throwGlobalError'; | |||
import { withCurrentUser } from '../../../../components/hoc/withCurrentUser'; | |||
import { hasGlobalPermission } from '../../../../helpers/users'; | |||
import { getAppState, Store } from '../../../../store/rootReducer'; | |||
import { | |||
AlmKeys, | |||
AlmSettingsInstance, | |||
ProjectAlmBindingConfigurationErrors, | |||
ProjectAlmBindingResponse | |||
} from '../../../../types/alm-settings'; | |||
import { EditionKey } from '../../../../types/editions'; | |||
import { Permissions } from '../../../../types/permissions'; | |||
import PRDecorationBindingRenderer from './PRDecorationBindingRenderer'; | |||
type FormData = T.Omit<ProjectAlmBindingResponse, 'alm'>; | |||
@@ -48,6 +53,7 @@ interface StateProps { | |||
interface Props { | |||
component: T.Component; | |||
currentUser: T.CurrentUser; | |||
} | |||
interface State { | |||
@@ -57,9 +63,11 @@ interface State { | |||
isConfigured: boolean; | |||
isValid: boolean; | |||
loading: boolean; | |||
orignalData?: FormData; | |||
saving: boolean; | |||
success: boolean; | |||
originalData?: FormData; | |||
updating: boolean; | |||
successfullyUpdated: boolean; | |||
checkingConfiguration: boolean; | |||
configurationErrors?: ProjectAlmBindingConfigurationErrors; | |||
} | |||
const REQUIRED_FIELDS_BY_ALM: { | |||
@@ -81,8 +89,9 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
isConfigured: false, | |||
isValid: false, | |||
loading: true, | |||
saving: false, | |||
success: false | |||
updating: false, | |||
successfullyUpdated: false, | |||
checkingConfiguration: false | |||
}; | |||
componentDidMount() { | |||
@@ -108,7 +117,8 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
isConfigured: !!originalData, | |||
isValid: this.validateForm(newFormData), | |||
loading: false, | |||
orignalData: newFormData | |||
originalData: newFormData, | |||
configurationErrors: undefined | |||
}; | |||
}); | |||
} | |||
@@ -117,7 +127,8 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}); | |||
}) | |||
.then(() => this.checkConfiguration()); | |||
}; | |||
getProjectBinding(project: string): Promise<ProjectAlmBindingResponse | undefined> { | |||
@@ -131,13 +142,13 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
catchError = () => { | |||
if (this.mounted) { | |||
this.setState({ saving: false }); | |||
this.setState({ updating: false }); | |||
} | |||
}; | |||
handleReset = () => { | |||
const { component } = this.props; | |||
this.setState({ saving: true }); | |||
this.setState({ updating: true }); | |||
deleteProjectAlmBinding(component.key) | |||
.then(() => { | |||
if (this.mounted) { | |||
@@ -148,11 +159,12 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
slug: '', | |||
monorepo: false | |||
}, | |||
orignalData: undefined, | |||
originalData: undefined, | |||
isChanged: false, | |||
isConfigured: false, | |||
saving: false, | |||
success: true | |||
updating: false, | |||
successfullyUpdated: true, | |||
configurationErrors: undefined | |||
}); | |||
} | |||
}) | |||
@@ -233,8 +245,28 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
} | |||
} | |||
checkConfiguration = async () => { | |||
const { | |||
component: { key: projectKey } | |||
} = this.props; | |||
const { isConfigured } = this.state; | |||
if (!isConfigured) { | |||
return; | |||
} | |||
this.setState({ checkingConfiguration: true, configurationErrors: undefined }); | |||
const configurationErrors = await validateProjectAlmBinding(projectKey).catch(error => error); | |||
if (this.mounted) { | |||
this.setState({ checkingConfiguration: false, configurationErrors }); | |||
} | |||
}; | |||
handleSubmit = () => { | |||
this.setState({ saving: true }); | |||
this.setState({ updating: true }); | |||
const { | |||
formData: { key, ...additionalFields }, | |||
instances | |||
@@ -249,8 +281,8 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
.then(() => { | |||
if (this.mounted) { | |||
this.setState({ | |||
saving: false, | |||
success: true | |||
updating: false, | |||
successfullyUpdated: true | |||
}); | |||
} | |||
}) | |||
@@ -278,7 +310,7 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
} | |||
handleFieldChange = (id: keyof ProjectAlmBindingResponse, value: string | boolean) => { | |||
this.setState(({ formData, orignalData }) => { | |||
this.setState(({ formData, originalData }) => { | |||
const newFormData = { | |||
...formData, | |||
[id]: value | |||
@@ -287,8 +319,8 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
return { | |||
formData: newFormData, | |||
isValid: this.validateForm(newFormData), | |||
isChanged: !this.isDataSame(newFormData, orignalData || { key: '', monorepo: false }), | |||
success: false | |||
isChanged: !this.isDataSame(newFormData, originalData || { key: '', monorepo: false }), | |||
successfullyUpdated: false | |||
}; | |||
}); | |||
}; | |||
@@ -305,15 +337,21 @@ export class PRDecorationBinding extends React.PureComponent<Props & StateProps, | |||
); | |||
}; | |||
handleCheckConfiguration = async () => { | |||
await this.checkConfiguration(); | |||
}; | |||
render() { | |||
const { monorepoEnabled } = this.props; | |||
const { currentUser, monorepoEnabled } = this.props; | |||
return ( | |||
<PRDecorationBindingRenderer | |||
onFieldChange={this.handleFieldChange} | |||
onReset={this.handleReset} | |||
onSubmit={this.handleSubmit} | |||
onCheckConfiguration={this.handleCheckConfiguration} | |||
monorepoEnabled={monorepoEnabled} | |||
isSysAdmin={hasGlobalPermission(currentUser, Permissions.Admin)} | |||
{...this.state} | |||
/> | |||
); | |||
@@ -327,4 +365,4 @@ const mapStateToProps = (state: Store): StateProps => ({ | |||
) | |||
}); | |||
export default connect(mapStateToProps)(PRDecorationBinding); | |||
export default connect(mapStateToProps)(withCurrentUser(PRDecorationBinding)); |
@@ -28,7 +28,13 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker'; | |||
import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../../../types/alm-settings'; | |||
import { | |||
AlmSettingsInstance, | |||
ProjectAlmBindingConfigurationErrors, | |||
ProjectAlmBindingConfigurationErrorScope, | |||
ProjectAlmBindingResponse | |||
} from '../../../../types/alm-settings'; | |||
import { ALM_INTEGRATION } from '../AdditionalCategoryKeys'; | |||
import AlmSpecificForm from './AlmSpecificForm'; | |||
export interface PRDecorationBindingRendererProps { | |||
@@ -41,9 +47,13 @@ export interface PRDecorationBindingRendererProps { | |||
onFieldChange: (id: keyof ProjectAlmBindingResponse, value: string | boolean) => void; | |||
onReset: () => void; | |||
onSubmit: () => void; | |||
saving: boolean; | |||
success: boolean; | |||
updating: boolean; | |||
successfullyUpdated: boolean; | |||
monorepoEnabled: boolean; | |||
onCheckConfiguration: () => void; | |||
checkingConfiguration: boolean; | |||
configurationErrors?: ProjectAlmBindingConfigurationErrors; | |||
isSysAdmin: boolean; | |||
} | |||
function optionRenderer(instance: AlmSettingsInstance) { | |||
@@ -65,9 +75,12 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe | |||
isConfigured, | |||
isValid, | |||
loading, | |||
saving, | |||
success, | |||
monorepoEnabled | |||
updating, | |||
successfullyUpdated, | |||
monorepoEnabled, | |||
checkingConfiguration, | |||
configurationErrors, | |||
isSysAdmin | |||
} = props; | |||
if (loading) { | |||
@@ -155,25 +168,81 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe | |||
/> | |||
)} | |||
<div className="display-flex-center big-spacer-top"> | |||
<DeferredSpinner className="spacer-right" loading={saving} /> | |||
<div className="display-flex-center big-spacer-top action-section"> | |||
{isChanged && ( | |||
<SubmitButton className="spacer-right button-success" disabled={saving || !isValid}> | |||
<SubmitButton className="spacer-right button-success" disabled={updating || !isValid}> | |||
<span data-test="project-settings__alm-save">{translate('save')}</span> | |||
<DeferredSpinner className="spacer-left" loading={updating} /> | |||
</SubmitButton> | |||
)} | |||
{isConfigured && ( | |||
<Button className="spacer-right" onClick={props.onReset}> | |||
<span data-test="project-settings__alm-reset">{translate('reset_verb')}</span> | |||
</Button> | |||
)} | |||
{!saving && success && ( | |||
<span className="text-success"> | |||
{!updating && successfullyUpdated && ( | |||
<span className="text-success spacer-right"> | |||
<AlertSuccessIcon className="spacer-right" /> | |||
{translate('settings.state.saved')} | |||
</span> | |||
)} | |||
{isConfigured && ( | |||
<> | |||
<Button className="spacer-right" onClick={props.onReset}> | |||
<span data-test="project-settings__alm-reset">{translate('reset_verb')}</span> | |||
</Button> | |||
{!isChanged && ( | |||
<Button onClick={props.onCheckConfiguration} disabled={checkingConfiguration}> | |||
{translate('settings.pr_decoration.binding.check_configuration')} | |||
<DeferredSpinner className="spacer-left" loading={checkingConfiguration} /> | |||
</Button> | |||
)} | |||
</> | |||
)} | |||
</div> | |||
{!checkingConfiguration && configurationErrors?.errors && ( | |||
<Alert variant="error" display="inline" className="big-spacer-top"> | |||
<p className="spacer-bottom"> | |||
{translate('settings.pr_decoration.binding.check_configuration.failure')} | |||
</p> | |||
<ul className="list-styled"> | |||
{configurationErrors.errors.map((error, i) => ( | |||
// eslint-disable-next-line react/no-array-index-key | |||
<li key={i}>{error.msg}</li> | |||
))} | |||
</ul> | |||
{configurationErrors.scope === ProjectAlmBindingConfigurationErrorScope.Global && ( | |||
<p> | |||
{isSysAdmin ? ( | |||
<FormattedMessage | |||
id="settings.pr_decoration.binding.check_configuration.failure.check_global_settings" | |||
defaultMessage={translate( | |||
'settings.pr_decoration.binding.check_configuration.failure.check_global_settings' | |||
)} | |||
values={{ | |||
link: ( | |||
<Link | |||
to={{ | |||
pathname: '/admin/settings', | |||
query: { | |||
category: ALM_INTEGRATION, | |||
alm | |||
} | |||
}}> | |||
{translate( | |||
'settings.pr_decoration.binding.check_configuration.failure.check_global_settings.link' | |||
)} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
) : ( | |||
translate('settings.pr_decoration.binding.check_configuration.contact_admin') | |||
)} | |||
</p> | |||
)} | |||
</Alert> | |||
)} | |||
{isConfigured && !isChanged && !checkingConfiguration && !configurationErrors && ( | |||
<Alert variant="success" display="inline" className="big-spacer-top"> | |||
{translate('settings.pr_decoration.binding.check_configuration.success')} | |||
</Alert> | |||
)} | |||
</form> | |||
</div> | |||
); |
@@ -28,11 +28,22 @@ import { | |||
setProjectBitbucketBinding, | |||
setProjectBitbucketCloudBinding, | |||
setProjectGithubBinding, | |||
setProjectGitlabBinding | |||
setProjectGitlabBinding, | |||
validateProjectAlmBinding | |||
} from '../../../../../api/alm-settings'; | |||
import { mockComponent } from '../../../../../helpers/testMocks'; | |||
import { AlmKeys, AlmSettingsInstance } from '../../../../../types/alm-settings'; | |||
import { | |||
mockAlmSettingsInstance, | |||
mockProjectAlmBindingResponse | |||
} from '../../../../../helpers/mocks/alm-settings'; | |||
import { mockComponent, mockCurrentUser } from '../../../../../helpers/testMocks'; | |||
import { | |||
AlmKeys, | |||
AlmSettingsInstance, | |||
ProjectAlmBindingConfigurationErrors, | |||
ProjectAlmBindingConfigurationErrorScope | |||
} from '../../../../../types/alm-settings'; | |||
import { PRDecorationBinding } from '../PRDecorationBinding'; | |||
import PRDecorationBindingRenderer from '../PRDecorationBindingRenderer'; | |||
jest.mock('../../../../../api/alm-settings', () => ({ | |||
getAlmSettings: jest.fn().mockResolvedValue([]), | |||
@@ -42,7 +53,8 @@ jest.mock('../../../../../api/alm-settings', () => ({ | |||
setProjectGithubBinding: jest.fn().mockResolvedValue(undefined), | |||
setProjectGitlabBinding: jest.fn().mockResolvedValue(undefined), | |||
setProjectBitbucketCloudBinding: jest.fn().mockResolvedValue(undefined), | |||
deleteProjectAlmBinding: jest.fn().mockResolvedValue(undefined) | |||
deleteProjectAlmBinding: jest.fn().mockResolvedValue(undefined), | |||
validateProjectAlmBinding: jest.fn().mockResolvedValue(undefined) | |||
})); | |||
const PROJECT_KEY = 'project-key'; | |||
@@ -122,7 +134,7 @@ describe('handleSubmit', () => { | |||
summaryCommentEnabled, | |||
monorepo | |||
}); | |||
expect(wrapper.state().success).toBe(true); | |||
expect(wrapper.state().successfullyUpdated).toBe(true); | |||
}); | |||
it('should work for azure', async () => { | |||
@@ -146,7 +158,7 @@ describe('handleSubmit', () => { | |||
repositoryName: repository, | |||
monorepo | |||
}); | |||
expect(wrapper.state().success).toBe(true); | |||
expect(wrapper.state().successfullyUpdated).toBe(true); | |||
}); | |||
it('should work for bitbucket', async () => { | |||
@@ -167,7 +179,7 @@ describe('handleSubmit', () => { | |||
slug, | |||
monorepo | |||
}); | |||
expect(wrapper.state().success).toBe(true); | |||
expect(wrapper.state().successfullyUpdated).toBe(true); | |||
}); | |||
it('should work for gitlab', async () => { | |||
@@ -189,7 +201,7 @@ describe('handleSubmit', () => { | |||
repository, | |||
monorepo | |||
}); | |||
expect(wrapper.state().success).toBe(true); | |||
expect(wrapper.state().successfullyUpdated).toBe(true); | |||
}); | |||
it('should work for bitbucket cloud', async () => { | |||
@@ -213,7 +225,7 @@ describe('handleSubmit', () => { | |||
repository, | |||
monorepo | |||
}); | |||
expect(wrapper.state().success).toBe(true); | |||
expect(wrapper.state().successfullyUpdated).toBe(true); | |||
}); | |||
}); | |||
@@ -233,12 +245,12 @@ describe.each([[500], [404]])('For status %i', status => { | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ | |||
formData: newFormData, | |||
orignalData: undefined | |||
originalData: undefined | |||
}); | |||
wrapper.instance().handleSubmit(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.instance().state.orignalData).toBeUndefined(); | |||
expect(wrapper.instance().state.originalData).toBeUndefined(); | |||
wrapper.instance().handleReset(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.instance().state.formData).toEqual(newFormData); | |||
@@ -355,9 +367,64 @@ it('should validate form', async () => { | |||
}); | |||
}); | |||
it('should call the validation WS and store errors', async () => { | |||
(getAlmSettings as jest.Mock).mockResolvedValueOnce(mockAlmSettingsInstance()); | |||
(getProjectAlmBinding as jest.Mock).mockResolvedValueOnce( | |||
mockProjectAlmBindingResponse({ key: 'key' }) | |||
); | |||
const errors: ProjectAlmBindingConfigurationErrors = { | |||
scope: ProjectAlmBindingConfigurationErrorScope.Global, | |||
errors: [{ msg: 'Test' }, { msg: 'tesT' }] | |||
}; | |||
(validateProjectAlmBinding as jest.Mock).mockRejectedValueOnce(errors); | |||
const wrapper = shallowRender(); | |||
wrapper | |||
.find(PRDecorationBindingRenderer) | |||
.props() | |||
.onCheckConfiguration(); | |||
await waitAndUpdate(wrapper); | |||
expect(validateProjectAlmBinding).toHaveBeenCalledWith(PROJECT_KEY); | |||
expect(wrapper.state().configurationErrors).toBe(errors); | |||
}); | |||
it('should call the validation WS after loading', async () => { | |||
(getAlmSettings as jest.Mock).mockResolvedValueOnce([mockAlmSettingsInstance()]); | |||
(getProjectAlmBinding as jest.Mock).mockResolvedValueOnce( | |||
mockProjectAlmBindingResponse({ key: 'key ' }) | |||
); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(validateProjectAlmBinding).toHaveBeenCalled(); | |||
}); | |||
it('should call the validation WS upon saving', async () => { | |||
(getAlmSettings as jest.Mock).mockResolvedValueOnce([mockAlmSettingsInstance()]); | |||
(getProjectAlmBinding as jest.Mock).mockResolvedValueOnce( | |||
mockProjectAlmBindingResponse({ key: 'key ' }) | |||
); | |||
const wrapper = shallowRender(); | |||
wrapper.instance().handleFieldChange('key', 'key'); | |||
wrapper.instance().handleSubmit(); | |||
await waitAndUpdate(wrapper); | |||
expect(validateProjectAlmBinding).toHaveBeenCalled(); | |||
}); | |||
function shallowRender(props: Partial<PRDecorationBinding['props']> = {}) { | |||
return shallow<PRDecorationBinding>( | |||
<PRDecorationBinding | |||
currentUser={mockCurrentUser()} | |||
component={mockComponent({ key: PROJECT_KEY })} | |||
monorepoEnabled={false} | |||
{...props} |
@@ -21,65 +21,51 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { AlmKeys } from '../../../../../types/alm-settings'; | |||
import { | |||
AlmKeys, | |||
AlmSettingsInstance, | |||
ProjectAlmBindingConfigurationErrors, | |||
ProjectAlmBindingConfigurationErrorScope | |||
} from '../../../../../types/alm-settings'; | |||
import PRDecorationBindingRenderer, { | |||
PRDecorationBindingRendererProps | |||
} from '../PRDecorationBindingRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ loading: false })).toMatchSnapshot(); | |||
}); | |||
it('should render single instance correctly', () => { | |||
const singleInstance = { | |||
key: 'single', | |||
url: 'http://single.url', | |||
alm: AlmKeys.GitHub | |||
}; | |||
expect( | |||
shallowRender({ | |||
loading: false, | |||
instances: [singleInstance] | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
const urls = ['http://github.enterprise.com', 'http://bbs.enterprise.com']; | |||
const instances: AlmSettingsInstance[] = [ | |||
{ | |||
alm: AlmKeys.GitHub, | |||
key: 'i1', | |||
url: urls[0] | |||
}, | |||
{ | |||
alm: AlmKeys.GitHub, | |||
key: 'i2', | |||
url: urls[0] | |||
}, | |||
{ | |||
alm: AlmKeys.BitbucketServer, | |||
key: 'i3', | |||
url: urls[1] | |||
}, | |||
{ | |||
alm: AlmKeys.Azure, | |||
key: 'i4' | |||
} | |||
]; | |||
const configurationErrors: ProjectAlmBindingConfigurationErrors = { | |||
scope: ProjectAlmBindingConfigurationErrorScope.Global, | |||
errors: [{ msg: 'Test' }, { msg: 'tesT' }] | |||
}; | |||
it('should render multiple instances correctly', () => { | |||
const urls = ['http://github.enterprise.com', 'http://bbs.enterprise.com']; | |||
const instances = [ | |||
{ | |||
alm: AlmKeys.GitHub, | |||
key: 'i1', | |||
url: urls[0] | |||
}, | |||
it.each([ | |||
['when loading', { loading: true }], | |||
['with no ALM instances', {}], | |||
['with a single ALM instance', { instances: [instances[0]] }], | |||
['with an empty form', { instances }], | |||
[ | |||
'with a valid and saved form', | |||
{ | |||
alm: AlmKeys.GitHub, | |||
key: 'i2', | |||
url: urls[0] | |||
}, | |||
{ | |||
alm: AlmKeys.BitbucketServer, | |||
key: 'i3', | |||
url: urls[1] | |||
}, | |||
{ | |||
alm: AlmKeys.Azure, | |||
key: 'i4' | |||
} | |||
]; | |||
//unfilled | |||
expect( | |||
shallowRender({ | |||
instances, | |||
loading: false | |||
}) | |||
).toMatchSnapshot(); | |||
// filled | |||
expect( | |||
shallowRender({ | |||
formData: { | |||
key: 'i1', | |||
repository: 'account/repo', | |||
@@ -87,43 +73,62 @@ it('should render multiple instances correctly', () => { | |||
}, | |||
isChanged: false, | |||
isConfigured: true, | |||
instances | |||
} | |||
], | |||
[ | |||
'when there are configuration errors (non-admin user)', | |||
{ instances, isConfigured: true, configurationErrors } | |||
], | |||
[ | |||
'when there are configuration errors (admin user)', | |||
{ | |||
formData: { | |||
key: 'i1', | |||
repository: 'account/repo', | |||
monorepo: false | |||
}, | |||
instances, | |||
loading: false | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should display action state correctly', () => { | |||
const urls = ['http://url.com']; | |||
const instances = [{ key: 'key', url: urls[0], alm: AlmKeys.GitHub }]; | |||
expect(shallowRender({ instances, loading: false, saving: true })).toMatchSnapshot(); | |||
expect(shallowRender({ instances, loading: false, success: true })).toMatchSnapshot(); | |||
expect( | |||
shallowRender({ | |||
isConfigured: true, | |||
configurationErrors, | |||
isSysAdmin: true | |||
} | |||
], | |||
[ | |||
'when there are configuration errors (admin user) and error are at PROJECT level', | |||
{ | |||
instances, | |||
isValid: true, | |||
loading: false | |||
}) | |||
).toMatchSnapshot(); | |||
isConfigured: true, | |||
configurationErrors: { | |||
...configurationErrors, | |||
scope: ProjectAlmBindingConfigurationErrorScope.Project | |||
}, | |||
isSysAdmin: true | |||
} | |||
] | |||
])('should render correctly', (name: string, props: PRDecorationBindingRendererProps) => { | |||
expect(shallowRender(props)).toMatchSnapshot(name); | |||
}); | |||
it.each([ | |||
['updating', { updating: true }], | |||
['update is successfull', { successfullyUpdated: true }], | |||
['form is valid', { isValid: true }], | |||
['configuration is saved', { isConfigured: true }], | |||
['configuration check is in progress', { isConfigured: true, checkingConfiguration: true }] | |||
])( | |||
'should display action section correctly when', | |||
(name: string, props: PRDecorationBindingRendererProps) => { | |||
expect(shallowRender({ ...props, instances }).find('.action-section')).toMatchSnapshot(name); | |||
} | |||
); | |||
it('should render select options correctly', async () => { | |||
const instances = [ | |||
{ | |||
alm: AlmKeys.Azure, | |||
key: 'azure' | |||
}, | |||
{ | |||
alm: AlmKeys.GitHub, | |||
key: 'github', | |||
url: 'gh.url.com' | |||
} | |||
]; | |||
const wrapper = shallowRender({ loading: false, instances }); | |||
const wrapper = shallowRender({ instances }); | |||
await waitAndUpdate(wrapper); | |||
const optionRenderer = wrapper.find(Select).prop('optionRenderer'); | |||
const { optionRenderer } = wrapper.find(Select).props(); | |||
expect(optionRenderer!(instances[0])).toMatchSnapshot(); | |||
@@ -142,13 +147,16 @@ function shallowRender(props: Partial<PRDecorationBindingRendererProps> = {}) { | |||
isChanged={false} | |||
isConfigured={false} | |||
isValid={false} | |||
loading={true} | |||
loading={false} | |||
onFieldChange={jest.fn()} | |||
onReset={jest.fn()} | |||
onSubmit={jest.fn()} | |||
saving={false} | |||
success={false} | |||
updating={false} | |||
successfullyUpdated={false} | |||
monorepoEnabled={false} | |||
checkingConfiguration={false} | |||
onCheckConfiguration={jest.fn()} | |||
isSysAdmin={false} | |||
{...props} | |||
/> | |||
); |
@@ -2,6 +2,7 @@ | |||
exports[`should render correctly 1`] = ` | |||
<PRDecorationBindingRenderer | |||
checkingConfiguration={false} | |||
formData={ | |||
Object { | |||
"key": "", | |||
@@ -11,13 +12,15 @@ exports[`should render correctly 1`] = ` | |||
instances={Array []} | |||
isChanged={false} | |||
isConfigured={false} | |||
isSysAdmin={false} | |||
isValid={false} | |||
loading={true} | |||
monorepoEnabled={false} | |||
onCheckConfiguration={[Function]} | |||
onFieldChange={[Function]} | |||
onReset={[Function]} | |||
onSubmit={[Function]} | |||
saving={false} | |||
success={false} | |||
successfullyUpdated={false} | |||
updating={false} | |||
/> | |||
`; |
@@ -1,6 +1,89 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display action state correctly 1`] = ` | |||
exports[`should display action section correctly when: configuration check is in progress 1`] = ` | |||
<div | |||
className="display-flex-center big-spacer-top action-section" | |||
> | |||
<Button | |||
className="spacer-right" | |||
onClick={[MockFunction]} | |||
> | |||
<span | |||
data-test="project-settings__alm-reset" | |||
> | |||
reset_verb | |||
</span> | |||
</Button> | |||
<Button | |||
disabled={true} | |||
onClick={[MockFunction]} | |||
> | |||
settings.pr_decoration.binding.check_configuration | |||
<DeferredSpinner | |||
className="spacer-left" | |||
loading={true} | |||
/> | |||
</Button> | |||
</div> | |||
`; | |||
exports[`should display action section correctly when: configuration is saved 1`] = ` | |||
<div | |||
className="display-flex-center big-spacer-top action-section" | |||
> | |||
<Button | |||
className="spacer-right" | |||
onClick={[MockFunction]} | |||
> | |||
<span | |||
data-test="project-settings__alm-reset" | |||
> | |||
reset_verb | |||
</span> | |||
</Button> | |||
<Button | |||
disabled={false} | |||
onClick={[MockFunction]} | |||
> | |||
settings.pr_decoration.binding.check_configuration | |||
<DeferredSpinner | |||
className="spacer-left" | |||
loading={false} | |||
/> | |||
</Button> | |||
</div> | |||
`; | |||
exports[`should display action section correctly when: form is valid 1`] = ` | |||
<div | |||
className="display-flex-center big-spacer-top action-section" | |||
/> | |||
`; | |||
exports[`should display action section correctly when: update is successfull 1`] = ` | |||
<div | |||
className="display-flex-center big-spacer-top action-section" | |||
> | |||
<span | |||
className="text-success spacer-right" | |||
> | |||
<AlertSuccessIcon | |||
className="spacer-right" | |||
/> | |||
settings.state.saved | |||
</span> | |||
</div> | |||
`; | |||
exports[`should display action section correctly when: updating 1`] = ` | |||
<div | |||
className="display-flex-center big-spacer-top action-section" | |||
/> | |||
`; | |||
exports[`should render correctly: when loading 1`] = `<DeferredSpinner />`; | |||
exports[`should render correctly: when there are configuration errors (admin user) 1`] = ` | |||
<div> | |||
<header | |||
className="page-header" | |||
@@ -63,31 +146,146 @@ exports[`should display action state correctly 1`] = ` | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
"url": "http://url.com", | |||
"key": "i1", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "github", | |||
"key": "i2", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "bitbucket", | |||
"key": "i3", | |||
"url": "http://bbs.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "azure", | |||
"key": "i4", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
value="i1" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
<AlmSpecificForm | |||
alm="github" | |||
formData={ | |||
Object { | |||
"key": "i1", | |||
"monorepo": false, | |||
"repository": "account/repo", | |||
} | |||
} | |||
instances={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "i1", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "github", | |||
"key": "i2", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "bitbucket", | |||
"key": "i3", | |||
"url": "http://bbs.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "azure", | |||
"key": "i4", | |||
}, | |||
] | |||
} | |||
monorepoEnabled={false} | |||
onFieldChange={[MockFunction]} | |||
/> | |||
<div | |||
className="display-flex-center big-spacer-top" | |||
className="display-flex-center big-spacer-top action-section" | |||
> | |||
<DeferredSpinner | |||
<Button | |||
className="spacer-right" | |||
loading={true} | |||
/> | |||
onClick={[MockFunction]} | |||
> | |||
<span | |||
data-test="project-settings__alm-reset" | |||
> | |||
reset_verb | |||
</span> | |||
</Button> | |||
<Button | |||
disabled={false} | |||
onClick={[MockFunction]} | |||
> | |||
settings.pr_decoration.binding.check_configuration | |||
<DeferredSpinner | |||
className="spacer-left" | |||
loading={false} | |||
/> | |||
</Button> | |||
</div> | |||
<Alert | |||
className="big-spacer-top" | |||
display="inline" | |||
variant="error" | |||
> | |||
<p | |||
className="spacer-bottom" | |||
> | |||
settings.pr_decoration.binding.check_configuration.failure | |||
</p> | |||
<ul | |||
className="list-styled" | |||
> | |||
<li | |||
key="0" | |||
> | |||
Test | |||
</li> | |||
<li | |||
key="1" | |||
> | |||
tesT | |||
</li> | |||
</ul> | |||
<p> | |||
<FormattedMessage | |||
defaultMessage="settings.pr_decoration.binding.check_configuration.failure.check_global_settings" | |||
id="settings.pr_decoration.binding.check_configuration.failure.check_global_settings" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/admin/settings", | |||
"query": Object { | |||
"alm": "github", | |||
"category": "almintegration", | |||
}, | |||
} | |||
} | |||
> | |||
settings.pr_decoration.binding.check_configuration.failure.check_global_settings.link | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</p> | |||
</Alert> | |||
</form> | |||
</div> | |||
`; | |||
exports[`should display action state correctly 2`] = ` | |||
exports[`should render correctly: when there are configuration errors (admin user) and error are at PROJECT level 1`] = ` | |||
<div> | |||
<header | |||
className="page-header" | |||
@@ -150,8 +348,22 @@ exports[`should display action state correctly 2`] = ` | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
"url": "http://url.com", | |||
"key": "i1", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "github", | |||
"key": "i2", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "bitbucket", | |||
"key": "i3", | |||
"url": "http://bbs.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "azure", | |||
"key": "i4", | |||
}, | |||
] | |||
} | |||
@@ -163,26 +375,59 @@ exports[`should display action state correctly 2`] = ` | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-center big-spacer-top" | |||
className="display-flex-center big-spacer-top action-section" | |||
> | |||
<DeferredSpinner | |||
<Button | |||
className="spacer-right" | |||
loading={false} | |||
/> | |||
<span | |||
className="text-success" | |||
onClick={[MockFunction]} | |||
> | |||
<AlertSuccessIcon | |||
className="spacer-right" | |||
<span | |||
data-test="project-settings__alm-reset" | |||
> | |||
reset_verb | |||
</span> | |||
</Button> | |||
<Button | |||
disabled={false} | |||
onClick={[MockFunction]} | |||
> | |||
settings.pr_decoration.binding.check_configuration | |||
<DeferredSpinner | |||
className="spacer-left" | |||
loading={false} | |||
/> | |||
settings.state.saved | |||
</span> | |||
</Button> | |||
</div> | |||
<Alert | |||
className="big-spacer-top" | |||
display="inline" | |||
variant="error" | |||
> | |||
<p | |||
className="spacer-bottom" | |||
> | |||
settings.pr_decoration.binding.check_configuration.failure | |||
</p> | |||
<ul | |||
className="list-styled" | |||
> | |||
<li | |||
key="0" | |||
> | |||
Test | |||
</li> | |||
<li | |||
key="1" | |||
> | |||
tesT | |||
</li> | |||
</ul> | |||
</Alert> | |||
</form> | |||
</div> | |||
`; | |||
exports[`should display action state correctly 3`] = ` | |||
exports[`should render correctly: when there are configuration errors (non-admin user) 1`] = ` | |||
<div> | |||
<header | |||
className="page-header" | |||
@@ -245,8 +490,22 @@ exports[`should display action state correctly 3`] = ` | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
"url": "http://url.com", | |||
"key": "i1", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "github", | |||
"key": "i2", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "bitbucket", | |||
"key": "i3", | |||
"url": "http://bbs.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "azure", | |||
"key": "i4", | |||
}, | |||
] | |||
} | |||
@@ -258,45 +517,62 @@ exports[`should display action state correctly 3`] = ` | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-center big-spacer-top" | |||
className="display-flex-center big-spacer-top action-section" | |||
> | |||
<DeferredSpinner | |||
<Button | |||
className="spacer-right" | |||
loading={false} | |||
/> | |||
onClick={[MockFunction]} | |||
> | |||
<span | |||
data-test="project-settings__alm-reset" | |||
> | |||
reset_verb | |||
</span> | |||
</Button> | |||
<Button | |||
disabled={false} | |||
onClick={[MockFunction]} | |||
> | |||
settings.pr_decoration.binding.check_configuration | |||
<DeferredSpinner | |||
className="spacer-left" | |||
loading={false} | |||
/> | |||
</Button> | |||
</div> | |||
<Alert | |||
className="big-spacer-top" | |||
display="inline" | |||
variant="error" | |||
> | |||
<p | |||
className="spacer-bottom" | |||
> | |||
settings.pr_decoration.binding.check_configuration.failure | |||
</p> | |||
<ul | |||
className="list-styled" | |||
> | |||
<li | |||
key="0" | |||
> | |||
Test | |||
</li> | |||
<li | |||
key="1" | |||
> | |||
tesT | |||
</li> | |||
</ul> | |||
<p> | |||
settings.pr_decoration.binding.check_configuration.contact_admin | |||
</p> | |||
</Alert> | |||
</form> | |||
</div> | |||
`; | |||
exports[`should render correctly 1`] = `<DeferredSpinner />`; | |||
exports[`should render correctly 2`] = ` | |||
<div> | |||
<Alert | |||
className="spacer-top huge-spacer-bottom" | |||
variant="info" | |||
> | |||
<FormattedMessage | |||
defaultMessage="settings.pr_decoration.binding.no_bindings" | |||
id="settings.pr_decoration.binding.no_bindings" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/analysis/pull-request/#pr-decoration" | |||
> | |||
learn_more | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render multiple instances correctly 1`] = ` | |||
exports[`should render correctly: with a single ALM instance 1`] = ` | |||
<div> | |||
<header | |||
className="page-header" | |||
@@ -362,20 +638,6 @@ exports[`should render multiple instances correctly 1`] = ` | |||
"key": "i1", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "github", | |||
"key": "i2", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "bitbucket", | |||
"key": "i3", | |||
"url": "http://bbs.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "azure", | |||
"key": "i4", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
@@ -386,18 +648,13 @@ exports[`should render multiple instances correctly 1`] = ` | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-center big-spacer-top" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
loading={false} | |||
/> | |||
</div> | |||
className="display-flex-center big-spacer-top action-section" | |||
/> | |||
</form> | |||
</div> | |||
`; | |||
exports[`should render multiple instances correctly 2`] = ` | |||
exports[`should render correctly: with a valid and saved form 1`] = ` | |||
<div> | |||
<header | |||
className="page-header" | |||
@@ -522,12 +779,8 @@ exports[`should render multiple instances correctly 2`] = ` | |||
onFieldChange={[MockFunction]} | |||
/> | |||
<div | |||
className="display-flex-center big-spacer-top" | |||
className="display-flex-center big-spacer-top action-section" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
loading={false} | |||
/> | |||
<Button | |||
className="spacer-right" | |||
onClick={[MockFunction]} | |||
@@ -538,32 +791,29 @@ exports[`should render multiple instances correctly 2`] = ` | |||
reset_verb | |||
</span> | |||
</Button> | |||
<Button | |||
disabled={false} | |||
onClick={[MockFunction]} | |||
> | |||
settings.pr_decoration.binding.check_configuration | |||
<DeferredSpinner | |||
className="spacer-left" | |||
loading={false} | |||
/> | |||
</Button> | |||
</div> | |||
<Alert | |||
className="big-spacer-top" | |||
display="inline" | |||
variant="success" | |||
> | |||
settings.pr_decoration.binding.check_configuration.success | |||
</Alert> | |||
</form> | |||
</div> | |||
`; | |||
exports[`should render select options correctly 1`] = ` | |||
<span> | |||
azure | |||
</span> | |||
`; | |||
exports[`should render select options correctly 2`] = ` | |||
<React.Fragment> | |||
<span> | |||
github | |||
— | |||
</span> | |||
<span | |||
className="text-muted" | |||
> | |||
gh.url.com | |||
</span> | |||
</React.Fragment> | |||
`; | |||
exports[`should render single instance correctly 1`] = ` | |||
exports[`should render correctly: with an empty form 1`] = ` | |||
<div> | |||
<header | |||
className="page-header" | |||
@@ -626,8 +876,22 @@ exports[`should render single instance correctly 1`] = ` | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "single", | |||
"url": "http://single.url", | |||
"key": "i1", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "github", | |||
"key": "i2", | |||
"url": "http://github.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "bitbucket", | |||
"key": "i3", | |||
"url": "http://bbs.enterprise.com", | |||
}, | |||
Object { | |||
"alm": "azure", | |||
"key": "i4", | |||
}, | |||
] | |||
} | |||
@@ -639,13 +903,61 @@ exports[`should render single instance correctly 1`] = ` | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-center big-spacer-top" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
loading={false} | |||
/> | |||
</div> | |||
className="display-flex-center big-spacer-top action-section" | |||
/> | |||
</form> | |||
</div> | |||
`; | |||
exports[`should render correctly: with no ALM instances 1`] = ` | |||
<div> | |||
<Alert | |||
className="spacer-top huge-spacer-bottom" | |||
variant="info" | |||
> | |||
<FormattedMessage | |||
defaultMessage="settings.pr_decoration.binding.no_bindings" | |||
id="settings.pr_decoration.binding.no_bindings" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/analysis/pull-request/#pr-decoration" | |||
> | |||
learn_more | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render select options correctly 1`] = ` | |||
<React.Fragment> | |||
<span> | |||
i1 | |||
— | |||
</span> | |||
<span | |||
className="text-muted" | |||
> | |||
http://github.enterprise.com | |||
</span> | |||
</React.Fragment> | |||
`; | |||
exports[`should render select options correctly 2`] = ` | |||
<React.Fragment> | |||
<span> | |||
i2 | |||
— | |||
</span> | |||
<span | |||
className="text-muted" | |||
> | |||
http://github.enterprise.com | |||
</span> | |||
</React.Fragment> | |||
`; |
@@ -153,6 +153,17 @@ export enum AlmSettingsBindingStatusType { | |||
Warning | |||
} | |||
export enum ProjectAlmBindingConfigurationErrorScope { | |||
Global = 'GLOBAL', | |||
Project = 'PROJECT', | |||
Unknown = 'UNKNOWN' | |||
} | |||
export interface ProjectAlmBindingConfigurationErrors { | |||
scope: ProjectAlmBindingConfigurationErrorScope; | |||
errors: { msg: string }[]; | |||
} | |||
export function isProjectBitbucketBindingResponse( | |||
binding: ProjectAlmBindingResponse | |||
): binding is ProjectBitbucketBindingResponse { |
@@ -1168,7 +1168,12 @@ settings.pr_decoration.binding.category=DevOps Platform Integration | |||
settings.pr_decoration.binding.no_bindings=This feature must first be enabled in the global settings. {link} | |||
settings.pr_decoration.binding.title=DevOps Platform Integration | |||
settings.pr_decoration.binding.description=Display your Quality Gate status directly in your DevOps Platform. | |||
settings.pr_decoration.binding.form.url=Project location | |||
settings.pr_decoration.binding.check_configuration=Check configuration | |||
settings.pr_decoration.binding.check_configuration.failure=You have the following errors in your configuration: | |||
settings.pr_decoration.binding.check_configuration.failure.check_global_settings=Please check your {link}. | |||
settings.pr_decoration.binding.check_configuration.failure.check_global_settings.link=global settings | |||
settings.pr_decoration.binding.check_configuration.contact_admin=Please contact your system administrator. | |||
settings.pr_decoration.binding.check_configuration.success=Configuration valid. | |||
settings.pr_decoration.binding.form.name=Configuration name | |||
settings.pr_decoration.binding.form.name.help=Each DevOps Platform instance must be configured globally first, and given a unique name. Pick the instance your project is hosted on. | |||
settings.pr_decoration.binding.form.monorepo=Enable mono repository support |