@@ -20,52 +20,80 @@ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; | |||
import { AlmKeys, ProjectAlmBindingResponse } from '../../../../types/alm-settings'; | |||
import { convertGithubApiUrlToLink, stripTrailingSlash } from '../../../../helpers/urls'; | |||
import { | |||
AlmKeys, | |||
AlmSettingsInstance, | |||
ProjectAlmBindingResponse | |||
} from '../../../../types/alm-settings'; | |||
import InputForBoolean from '../inputs/InputForBoolean'; | |||
export interface AlmSpecificFormProps { | |||
alm: AlmKeys; | |||
instances: AlmSettingsInstance[]; | |||
formData: T.Omit<ProjectAlmBindingResponse, 'alm'>; | |||
onFieldChange: (id: keyof ProjectAlmBindingResponse, value: string | boolean) => void; | |||
monorepoEnabled: boolean; | |||
} | |||
interface LabelProps { | |||
help?: boolean; | |||
helpParams?: T.Dict<string | JSX.Element>; | |||
id: string; | |||
optional?: boolean; | |||
} | |||
interface CommonFieldProps extends LabelProps { | |||
help?: boolean; | |||
helpParams?: T.Dict<string | JSX.Element>; | |||
helpExample?: JSX.Element; | |||
onFieldChange: (id: keyof ProjectAlmBindingResponse, value: string | boolean) => void; | |||
propKey: keyof ProjectAlmBindingResponse; | |||
} | |||
function renderFieldWrapper( | |||
label: React.ReactNode, | |||
input: React.ReactNode, | |||
help?: React.ReactNode | |||
) { | |||
return ( | |||
<div className="settings-definition"> | |||
<div className="settings-definition-left"> | |||
{label} | |||
{help && <div className="markdown small spacer-top">{help}</div>} | |||
</div> | |||
<div className="settings-definition-right padded-top">{input}</div> | |||
</div> | |||
); | |||
} | |||
function renderHelp({ help, helpExample, helpParams, id }: CommonFieldProps) { | |||
return ( | |||
help && ( | |||
<> | |||
<FormattedMessage | |||
defaultMessage={translate('settings.pr_decoration.binding.form', id, 'help')} | |||
id={`settings.pr_decoration.binding.form.${id}.help`} | |||
values={helpParams} | |||
/> | |||
{helpExample && ( | |||
<div className="spacer-top nowrap"> | |||
{translate('example')}: <em>{helpExample}</em> | |||
</div> | |||
)} | |||
</> | |||
) | |||
); | |||
} | |||
function renderLabel(props: LabelProps) { | |||
const { help, helpParams, optional, id } = props; | |||
const { optional, id } = props; | |||
return ( | |||
<label className="display-flex-center" htmlFor={id}> | |||
<label className="h3" htmlFor={id}> | |||
{translate('settings.pr_decoration.binding.form', id)} | |||
{!optional && <MandatoryFieldMarker />} | |||
{help && ( | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<FormattedMessage | |||
defaultMessage={translate('settings.pr_decoration.binding.form', id, 'help')} | |||
id={`settings.pr_decoration.binding.form.${id}.help`} | |||
values={helpParams} | |||
/> | |||
} | |||
placement="right" | |||
/> | |||
)} | |||
</label> | |||
); | |||
} | |||
@@ -77,19 +105,18 @@ function renderBooleanField( | |||
} | |||
) { | |||
const { id, value, onFieldChange, propKey, inputExtra } = props; | |||
return ( | |||
<div className="form-field"> | |||
{renderLabel({ ...props, optional: true })} | |||
<div className="display-flex-center"> | |||
<InputForBoolean | |||
isDefault={true} | |||
name={id} | |||
onChange={v => onFieldChange(propKey, v)} | |||
value={value} | |||
/> | |||
{inputExtra} | |||
</div> | |||
</div> | |||
return renderFieldWrapper( | |||
renderLabel({ ...props, optional: true }), | |||
<div className="display-flex-center big-spacer-top"> | |||
<InputForBoolean | |||
isDefault={true} | |||
name={id} | |||
onChange={v => onFieldChange(propKey, v)} | |||
value={value} | |||
/> | |||
{inputExtra} | |||
</div>, | |||
renderHelp(props) | |||
); | |||
} | |||
@@ -99,30 +126,31 @@ function renderField( | |||
} | |||
) { | |||
const { id, propKey, value, onFieldChange } = props; | |||
return ( | |||
<div className="form-field"> | |||
{renderLabel(props)} | |||
<input | |||
className="input-super-large" | |||
id={id} | |||
maxLength={256} | |||
name={id} | |||
onChange={e => onFieldChange(propKey, e.currentTarget.value)} | |||
type="text" | |||
value={value} | |||
/> | |||
</div> | |||
return renderFieldWrapper( | |||
renderLabel(props), | |||
<input | |||
className="input-super-large big-spacer-top" | |||
id={id} | |||
maxLength={256} | |||
name={id} | |||
onChange={e => onFieldChange(propKey, e.currentTarget.value)} | |||
type="text" | |||
value={value} | |||
/>, | |||
renderHelp(props) | |||
); | |||
} | |||
export default function AlmSpecificForm(props: AlmSpecificFormProps) { | |||
const { | |||
alm, | |||
instances, | |||
formData: { repository, slug, summaryCommentEnabled, monorepo }, | |||
monorepoEnabled | |||
} = props; | |||
let formFields: JSX.Element; | |||
const instance = instances.find(i => i.alm === alm); | |||
switch (alm) { | |||
case AlmKeys.Azure: | |||
@@ -130,6 +158,7 @@ export default function AlmSpecificForm(props: AlmSpecificFormProps) { | |||
<> | |||
{renderField({ | |||
help: true, | |||
helpExample: <strong>My Project</strong>, | |||
id: 'azure.project', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'slug', | |||
@@ -137,6 +166,7 @@ export default function AlmSpecificForm(props: AlmSpecificFormProps) { | |||
})} | |||
{renderField({ | |||
help: true, | |||
helpExample: <strong>My Repository</strong>, | |||
id: 'azure.repository', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'repository', | |||
@@ -150,15 +180,15 @@ export default function AlmSpecificForm(props: AlmSpecificFormProps) { | |||
<> | |||
{renderField({ | |||
help: true, | |||
helpParams: { | |||
example: ( | |||
<> | |||
{'.../projects/'} | |||
<strong>{'{KEY}'}</strong> | |||
{'/repos/{SLUG}/browse'} | |||
</> | |||
) | |||
}, | |||
helpExample: ( | |||
<> | |||
{instance?.url | |||
? `${stripTrailingSlash(instance.url)}/projects/` | |||
: 'https://bb.company.com/projects/'} | |||
<strong>{'MY_PROJECT_KEY'}</strong> | |||
{'/repos/my-repository-slug/browse'} | |||
</> | |||
), | |||
id: 'bitbucket.repository', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'repository', | |||
@@ -166,15 +196,15 @@ export default function AlmSpecificForm(props: AlmSpecificFormProps) { | |||
})} | |||
{renderField({ | |||
help: true, | |||
helpParams: { | |||
example: ( | |||
<> | |||
{'.../projects/{KEY}/repos/'} | |||
<strong>{'{SLUG}'}</strong> | |||
{'/browse'} | |||
</> | |||
) | |||
}, | |||
helpExample: ( | |||
<> | |||
{instance?.url | |||
? `${stripTrailingSlash(instance.url)}/projects/MY_PROJECT_KEY/repos/` | |||
: 'https://bb.company.com/projects/MY_PROJECT_KEY/repos/'} | |||
<strong>{'my-repository-slug'}</strong> | |||
{'/browse'} | |||
</> | |||
), | |||
id: 'bitbucket.slug', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'slug', | |||
@@ -188,14 +218,12 @@ export default function AlmSpecificForm(props: AlmSpecificFormProps) { | |||
<> | |||
{renderField({ | |||
help: true, | |||
helpParams: { | |||
example: ( | |||
<> | |||
{'https://bitbucket.org/{workspace}/'} | |||
<strong>{'{repository}'}</strong> | |||
</> | |||
) | |||
}, | |||
helpExample: ( | |||
<> | |||
{'https://bitbucket.org/my-workspace/'} | |||
<strong>{'my-repository-slug'}</strong> | |||
</> | |||
), | |||
id: 'bitbucketcloud.repository', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'repository', | |||
@@ -209,7 +237,14 @@ export default function AlmSpecificForm(props: AlmSpecificFormProps) { | |||
<> | |||
{renderField({ | |||
help: true, | |||
helpParams: { example: 'SonarSource/sonarqube' }, | |||
helpExample: ( | |||
<> | |||
{instance?.url | |||
? `${stripTrailingSlash(convertGithubApiUrlToLink(instance.url))}/` | |||
: 'https://github.com/'} | |||
<strong>{'sonarsource/sonarqube'}</strong> | |||
</> | |||
), | |||
id: 'github.repository', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'repository', | |||
@@ -229,6 +264,8 @@ export default function AlmSpecificForm(props: AlmSpecificFormProps) { | |||
formFields = ( | |||
<> | |||
{renderField({ | |||
help: true, | |||
helpExample: <strong>123456</strong>, | |||
id: 'gitlab.repository', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'repository', |
@@ -114,40 +114,48 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe | |||
}}> | |||
<MandatoryFieldsExplanation className="form-field" /> | |||
<div className="form-field"> | |||
<label htmlFor="name"> | |||
{translate('settings.pr_decoration.binding.form.name')} | |||
<MandatoryFieldMarker className="spacer-right" /> | |||
</label> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={{ | |||
maxWidth: '210%' /* Allow double the width of the select */, | |||
width: 'auto' | |||
}} | |||
onChange={(instance: AlmSettingsInstance) => props.onFieldChange('key', instance.key)} | |||
optionRenderer={optionRenderer} | |||
options={instances} | |||
searchable={false} | |||
value={formData.key} | |||
valueKey="key" | |||
valueRenderer={optionRenderer} | |||
/> | |||
<div className="settings-definition big-spacer-bottom"> | |||
<div className="settings-definition-left"> | |||
<label className="h3" htmlFor="name"> | |||
{translate('settings.pr_decoration.binding.form.name')} | |||
<MandatoryFieldMarker className="spacer-right" /> | |||
</label> | |||
<div className="markdown small spacer-top"> | |||
{translate('settings.pr_decoration.binding.form.name.help')} | |||
</div> | |||
</div> | |||
<div className="settings-definition-right"> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400 big-spacer-top" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={{ | |||
maxWidth: '210%' /* Allow double the width of the select */, | |||
width: 'auto' | |||
}} | |||
onChange={(instance: AlmSettingsInstance) => props.onFieldChange('key', instance.key)} | |||
optionRenderer={optionRenderer} | |||
options={instances} | |||
searchable={false} | |||
value={formData.key} | |||
valueKey="key" | |||
valueRenderer={optionRenderer} | |||
/> | |||
</div> | |||
</div> | |||
{alm && ( | |||
<AlmSpecificForm | |||
alm={alm} | |||
instances={instances} | |||
formData={formData} | |||
onFieldChange={props.onFieldChange} | |||
monorepoEnabled={monorepoEnabled} | |||
/> | |||
)} | |||
<div className="display-flex-center"> | |||
<div className="display-flex-center big-spacer-top"> | |||
<DeferredSpinner className="spacer-right" loading={saving} /> | |||
{isChanged && ( | |||
<SubmitButton className="spacer-right button-success" disabled={saving || !isValid}> |
@@ -19,7 +19,8 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { AlmKeys } from '../../../../../types/alm-settings'; | |||
import { mockAlmSettingsInstance } from '../../../../../helpers/mocks/alm-settings'; | |||
import { AlmKeys, AlmSettingsInstance } from '../../../../../types/alm-settings'; | |||
import AlmSpecificForm, { AlmSpecificFormProps } from '../AlmSpecificForm'; | |||
it.each([ | |||
@@ -32,6 +33,20 @@ it.each([ | |||
expect(shallowRender(alm)).toMatchSnapshot(); | |||
}); | |||
it.each([ | |||
[ | |||
AlmKeys.BitbucketServer, | |||
[mockAlmSettingsInstance({ alm: AlmKeys.BitbucketServer, url: 'http://bbs.example.com' })] | |||
], | |||
[AlmKeys.GitHub, [mockAlmSettingsInstance({ url: 'http://example.com/api/v3' })]], | |||
[AlmKeys.GitHub, [mockAlmSettingsInstance({ url: 'http://api.github.com' })]] | |||
])( | |||
'it should render correctly for %s if an instance URL is provided', | |||
(alm: AlmKeys, instances: AlmSettingsInstance[]) => { | |||
expect(shallowRender(alm, { instances })).toMatchSnapshot(); | |||
} | |||
); | |||
it('should render the monorepo field when the feature is supported', () => { | |||
expect(shallowRender(AlmKeys.Azure, { monorepoEnabled: true })).toMatchSnapshot(); | |||
}); | |||
@@ -40,6 +55,7 @@ function shallowRender(alm: AlmKeys, props: Partial<AlmSpecificFormProps> = {}) | |||
return shallow( | |||
<AlmSpecificForm | |||
alm={alm} | |||
instances={[]} | |||
formData={{ | |||
key: '', | |||
repository: '', |
@@ -23,46 +23,60 @@ exports[`should display action state correctly 1`] = ` | |||
className="form-field" | |||
/> | |||
<div | |||
className="form-field" | |||
className="settings-definition big-spacer-bottom" | |||
> | |||
<label | |||
htmlFor="name" | |||
<div | |||
className="settings-definition-left" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
<label | |||
className="h3" | |||
htmlFor="name" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<div | |||
className="markdown small spacer-top" | |||
> | |||
settings.pr_decoration.binding.form.name.help | |||
</div> | |||
</div> | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400 big-spacer-top" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
"url": "http://url.com", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
"url": "http://url.com", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
className="display-flex-center big-spacer-top" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
@@ -96,46 +110,60 @@ exports[`should display action state correctly 2`] = ` | |||
className="form-field" | |||
/> | |||
<div | |||
className="form-field" | |||
className="settings-definition big-spacer-bottom" | |||
> | |||
<label | |||
htmlFor="name" | |||
<div | |||
className="settings-definition-left" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
<label | |||
className="h3" | |||
htmlFor="name" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<div | |||
className="markdown small spacer-top" | |||
> | |||
settings.pr_decoration.binding.form.name.help | |||
</div> | |||
</div> | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400 big-spacer-top" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
"url": "http://url.com", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
"url": "http://url.com", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
className="display-flex-center big-spacer-top" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
@@ -177,46 +205,60 @@ exports[`should display action state correctly 3`] = ` | |||
className="form-field" | |||
/> | |||
<div | |||
className="form-field" | |||
className="settings-definition big-spacer-bottom" | |||
> | |||
<label | |||
htmlFor="name" | |||
<div | |||
className="settings-definition-left" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
<label | |||
className="h3" | |||
htmlFor="name" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<div | |||
className="markdown small spacer-top" | |||
> | |||
settings.pr_decoration.binding.form.name.help | |||
</div> | |||
</div> | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400 big-spacer-top" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
"url": "http://url.com", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "key", | |||
"url": "http://url.com", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
className="display-flex-center big-spacer-top" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
@@ -277,60 +319,74 @@ exports[`should render multiple instances correctly 1`] = ` | |||
className="form-field" | |||
/> | |||
<div | |||
className="form-field" | |||
className="settings-definition big-spacer-bottom" | |||
> | |||
<label | |||
htmlFor="name" | |||
<div | |||
className="settings-definition-left" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
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", | |||
}, | |||
<label | |||
className="h3" | |||
htmlFor="name" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<div | |||
className="markdown small spacer-top" | |||
> | |||
settings.pr_decoration.binding.form.name.help | |||
</div> | |||
</div> | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400 big-spacer-top" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"alm": "azure", | |||
"key": "i4", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
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", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
className="display-flex-center big-spacer-top" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
@@ -364,57 +420,71 @@ exports[`should render multiple instances correctly 2`] = ` | |||
className="form-field" | |||
/> | |||
<div | |||
className="form-field" | |||
className="settings-definition big-spacer-bottom" | |||
> | |||
<label | |||
htmlFor="name" | |||
<div | |||
className="settings-definition-left" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
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", | |||
}, | |||
<label | |||
className="h3" | |||
htmlFor="name" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<div | |||
className="markdown small spacer-top" | |||
> | |||
settings.pr_decoration.binding.form.name.help | |||
</div> | |||
</div> | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400 big-spacer-top" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"alm": "azure", | |||
"key": "i4", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="i1" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
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", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="i1" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
<AlmSpecificForm | |||
alm="github" | |||
@@ -425,11 +495,34 @@ exports[`should render multiple instances correctly 2`] = ` | |||
"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" | |||
className="display-flex-center big-spacer-top" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
@@ -493,46 +586,60 @@ exports[`should render single instance correctly 1`] = ` | |||
className="form-field" | |||
/> | |||
<div | |||
className="form-field" | |||
className="settings-definition big-spacer-bottom" | |||
> | |||
<label | |||
htmlFor="name" | |||
<div | |||
className="settings-definition-left" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
<label | |||
className="h3" | |||
htmlFor="name" | |||
> | |||
settings.pr_decoration.binding.form.name | |||
<MandatoryFieldMarker | |||
className="spacer-right" | |||
/> | |||
</label> | |||
<div | |||
className="markdown small spacer-top" | |||
> | |||
settings.pr_decoration.binding.form.name.help | |||
</div> | |||
</div> | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<Select | |||
autosize={true} | |||
className="abs-width-400 big-spacer-top" | |||
clearable={false} | |||
id="name" | |||
menuContainerStyle={ | |||
Object { | |||
"alm": "github", | |||
"key": "single", | |||
"url": "http://single.url", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
"maxWidth": "210%", | |||
"width": "auto", | |||
} | |||
} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"alm": "github", | |||
"key": "single", | |||
"url": "http://single.url", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="" | |||
valueKey="key" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
className="display-flex-center big-spacer-top" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" |
@@ -17,6 +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 { convertGithubApiUrlToLink, stripTrailingSlash } from '../../helpers/urls'; | |||
import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings'; | |||
export function quote(os: string): (s: string) => string { | |||
@@ -64,10 +65,7 @@ export function buildGithubLink( | |||
} | |||
// strip the api path: | |||
const urlRoot = almBinding.url | |||
.replace(/\/api\/v\d+\/?$/, '') // GH Enterprise | |||
.replace(/^https?:\/\/api\.github\.com/, 'https://github.com') // GH.com | |||
.replace(/\/$/, ''); | |||
const urlRoot = convertGithubApiUrlToLink(almBinding.url); | |||
return `${urlRoot}/${projectBinding.repository}`; | |||
return `${stripTrailingSlash(urlRoot)}/${projectBinding.repository}`; | |||
} |
@@ -20,19 +20,37 @@ | |||
import { ComponentQualifier } from '../../types/component'; | |||
import { IssueType } from '../../types/issues'; | |||
import { | |||
convertGithubApiUrlToLink, | |||
getComponentDrilldownUrl, | |||
getComponentIssuesUrl, | |||
getComponentOverviewUrl, | |||
getComponentSecurityHotspotsUrl, | |||
getIssuesUrl, | |||
getQualityGatesUrl, | |||
getQualityGateUrl | |||
getQualityGateUrl, | |||
stripTrailingSlash | |||
} from '../urls'; | |||
const SIMPLE_COMPONENT_KEY = 'sonarqube'; | |||
const COMPLEX_COMPONENT_KEY = 'org.sonarsource.sonarqube:sonarqube'; | |||
const METRIC = 'coverage'; | |||
describe('#convertGithubApiUrlToLink', () => { | |||
it('should correctly convert a GitHub API URL to a Web URL', () => { | |||
expect(convertGithubApiUrlToLink('https://api.github.com')).toBe('https://github.com'); | |||
expect(convertGithubApiUrlToLink('https://company.github.com/api/v3')).toBe( | |||
'https://company.github.com' | |||
); | |||
}); | |||
}); | |||
describe('#stripTrailingSlash', () => { | |||
it('should correctly strip trailing slashes from any URL', () => { | |||
expect(stripTrailingSlash('https://example.com/')).toBe('https://example.com'); | |||
expect(convertGithubApiUrlToLink('https://example.com')).toBe('https://example.com'); | |||
}); | |||
}); | |||
describe('#getComponentIssuesUrl', () => { | |||
it('should work without parameters', () => { | |||
expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, {})).toEqual({ |
@@ -280,3 +280,13 @@ export function getHomePageUrl(homepage: T.HomePage) { | |||
// should never happen, but just in case... | |||
return '/projects'; | |||
} | |||
export function convertGithubApiUrlToLink(url: string) { | |||
return url | |||
.replace(/^https?:\/\/api\.github\.com/, 'https://github.com') // GH.com | |||
.replace(/\/api\/v\d+\/?$/, ''); // GH Enterprise | |||
} | |||
export function stripTrailingSlash(url: string) { | |||
return url.replace(/\/$/, ''); | |||
} |
@@ -1170,25 +1170,27 @@ 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.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 | |||
settings.pr_decoration.binding.form.monorepo.help=Enable this setting if your project is part of a mono repository. {doc_link} | |||
settings.pr_decoration.binding.form.monorepo.warning=This setting must be enabled for all SonarQube projects that are part of a mono repository. | |||
settings.pr_decoration.binding.form.azure.project=Project Name | |||
settings.pr_decoration.binding.form.azure.project.help=The name of the Azure DevOps project containing your repository. | |||
settings.pr_decoration.binding.form.azure.repository=Repository Name | |||
settings.pr_decoration.binding.form.azure.repository.help=The name of your Azure DevOps repository. | |||
settings.pr_decoration.binding.form.github.repository=Repository identifier | |||
settings.pr_decoration.binding.form.github.repository.help=The path of your repository URL. Example: {example} | |||
settings.pr_decoration.binding.form.azure.project=Project name | |||
settings.pr_decoration.binding.form.azure.project.help=The name of the Azure DevOps project containing your repository. You can find this name on your project's Overview page. | |||
settings.pr_decoration.binding.form.azure.repository=Repository name | |||
settings.pr_decoration.binding.form.azure.repository.help=The name of your Azure DevOps repository. You can find this name on your project's Repos page. | |||
settings.pr_decoration.binding.form.github.repository=Repository name | |||
settings.pr_decoration.binding.form.github.repository.help=The full name of your repository, including the organization. You can find this name in your repository's URL. This name is case-sensitive! | |||
settings.pr_decoration.binding.form.github.summary_comment_setting=Enable analysis summary under the GitHub Conversation tab | |||
settings.pr_decoration.binding.form.github.summary_comment_setting.help=When enabled, a summary is displayed under the GitHub Conversation tab. Notifications may be sent by GitHub depending on your settings. | |||
settings.pr_decoration.binding.form.bitbucket.repository=Project Key | |||
settings.pr_decoration.binding.form.bitbucket.repository.help=The project key is part of your Bitbucket Server repository URL. Example: ({example}) | |||
settings.pr_decoration.binding.form.bitbucket.slug=Repository SLUG | |||
settings.pr_decoration.binding.form.bitbucket.slug.help=The Repository Slug is part of your Bitbucket Server repository URL. Example: ({example}) | |||
settings.pr_decoration.binding.form.bitbucketcloud.repository=Repository SLUG | |||
settings.pr_decoration.binding.form.bitbucketcloud.repository.help=The Repository SLUG is part of your Bitbucket Cloud URL. Example: {example} | |||
settings.pr_decoration.binding.form.bitbucket.repository=Project key | |||
settings.pr_decoration.binding.form.bitbucket.repository.help=The project key is part of your Bitbucket Server repository URL. This is case-sensitive! | |||
settings.pr_decoration.binding.form.bitbucket.slug=Repository slug | |||
settings.pr_decoration.binding.form.bitbucket.slug.help=The repository slug is part of your Bitbucket Server repository URL. This slug is case-sensitive! | |||
settings.pr_decoration.binding.form.bitbucketcloud.repository=Repository slug | |||
settings.pr_decoration.binding.form.bitbucketcloud.repository.help=The repository slug is part of your Bitbucket Cloud repository URL. | |||
settings.pr_decoration.binding.form.gitlab.repository=Project ID | |||
settings.pr_decoration.binding.form.gitlab.repository.help=The Project ID is a numerical unique identifier for your project. You can find it on your Project Overview. | |||
property.category.general=General | |||
property.category.general.email=Email |