Bläddra i källkod

SONAR-14873 Ease comprehension of PR Decoration settings form

tags/9.0.0.45539
Wouter Admiraal 3 år sedan
förälder
incheckning
06681cd89f

+ 109
- 72
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx Visa fil

@@ -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',

+ 31
- 23
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx Visa fil

@@ -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}>

+ 17
- 1
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/AlmSpecificForm-test.tsx Visa fil

@@ -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: '',

+ 759
- 323
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 345
- 238
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap Visa fil

@@ -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"

+ 3
- 5
server/sonar-web/src/main/js/components/tutorials/utils.ts Visa fil

@@ -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}`;
}

+ 19
- 1
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts Visa fil

@@ -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({

+ 10
- 0
server/sonar-web/src/main/js/helpers/urls.ts Visa fil

@@ -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(/\/$/, '');
}

+ 14
- 12
sonar-core/src/main/resources/org/sonar/l10n/core.properties Visa fil

@@ -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

Laddar…
Avbryt
Spara