@@ -18,7 +18,8 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { BadgeColors, BadgeType, BadgeOptions } from './utils'; | |||
import * as classNames from 'classnames'; | |||
import { BadgeColors, BadgeType, BadgeOptions, BadgeFormats } from './utils'; | |||
import Select from '../../../components/controls/Select'; | |||
import { fetchWebApi } from '../../../api/web-api'; | |||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
@@ -65,66 +66,105 @@ export default class BadgeParams extends React.PureComponent<Props> { | |||
); | |||
} | |||
getColorOptions = () => | |||
['white', 'black', 'orange'].map(color => ({ | |||
getColorOptions = () => { | |||
return ['white', 'black', 'orange'].map(color => ({ | |||
label: translate('overview.badges.options.colors', color), | |||
value: color | |||
})); | |||
}; | |||
getMetricOptions = () => | |||
this.state.badgeMetrics.map(key => { | |||
getFormatOptions = () => { | |||
return ['md', 'url'].map(format => ({ | |||
label: translate('overview.badges.options.formats', format), | |||
value: format | |||
})); | |||
}; | |||
getMetricOptions = () => { | |||
return this.state.badgeMetrics.map(key => { | |||
const metric = this.props.metrics[key]; | |||
return { | |||
value: key, | |||
label: metric ? getLocalizedMetricName(metric) : key | |||
}; | |||
}); | |||
}; | |||
handleColorChange = ({ value }: { value: BadgeColors }) => | |||
handleColorChange = ({ value }: { value: BadgeColors }) => { | |||
this.props.updateOptions({ color: value }); | |||
}; | |||
handleMetricChange = ({ value }: { value: string }) => | |||
handleFormatChange = ({ value }: { value: BadgeFormats }) => { | |||
this.props.updateOptions({ format: value }); | |||
}; | |||
handleMetricChange = ({ value }: { value: string }) => { | |||
this.props.updateOptions({ metric: value }); | |||
}; | |||
renderBadgeType = (type: BadgeType, options: BadgeOptions) => { | |||
if (type === BadgeType.marketing) { | |||
return ( | |||
<> | |||
<label className="spacer-right" htmlFor="badge-color"> | |||
{translate('color')}: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
name="badge-color" | |||
onChange={this.handleColorChange} | |||
options={this.getColorOptions()} | |||
searchable={false} | |||
value={options.color} | |||
/> | |||
</> | |||
); | |||
} else if (type === BadgeType.measure) { | |||
return ( | |||
<> | |||
<label className="spacer-right" htmlFor="badge-metric"> | |||
{translate('overview.badges.metric')}: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
name="badge-metric" | |||
onChange={this.handleMetricChange} | |||
options={this.getMetricOptions()} | |||
searchable={false} | |||
value={options.metric} | |||
/> | |||
</> | |||
); | |||
} else { | |||
return null; | |||
} | |||
}; | |||
render() { | |||
const { className, options, type } = this.props; | |||
switch (type) { | |||
case BadgeType.marketing: | |||
return ( | |||
<div className={className}> | |||
<label className="big-spacer-right" htmlFor="badge-color"> | |||
{translate('color')} | |||
</label> | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
name="badge-color" | |||
onChange={this.handleColorChange} | |||
options={this.getColorOptions()} | |||
searchable={false} | |||
value={options.color} | |||
/> | |||
</div> | |||
); | |||
case BadgeType.measure: | |||
return ( | |||
<div className={className}> | |||
<label className="big-spacer-right" htmlFor="badge-metric"> | |||
{translate('overview.badges.metric')} | |||
</label> | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
name="badge-metric" | |||
onChange={this.handleMetricChange} | |||
options={this.getMetricOptions()} | |||
searchable={false} | |||
value={options.metric} | |||
/> | |||
</div> | |||
); | |||
default: | |||
return null; | |||
} | |||
return ( | |||
<div className={className}> | |||
{this.renderBadgeType(type, options)} | |||
<label | |||
className={classNames('spacer-right', { | |||
'big-spacer-left': type !== BadgeType.qualityGate | |||
})} | |||
htmlFor="badge-format"> | |||
{translate('format')}: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
name="badge-format" | |||
onChange={this.handleFormatChange} | |||
options={this.getFormatOptions()} | |||
searchable={false} | |||
value={this.props.options.format || 'md'} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import BadgeButton from './BadgeButton'; | |||
import BadgeParams from './BadgeParams'; | |||
import { BadgeType, BadgeOptions, getBadgeUrl } from './utils'; | |||
import { BadgeType, BadgeOptions, getBadgeUrl, getBadgeSnippet } from './utils'; | |||
import CodeSnippet from '../../../components/common/CodeSnippet'; | |||
import Modal from '../../../components/controls/Modal'; | |||
import { getBranchLikeQuery } from '../../../helpers/branches'; | |||
@@ -108,7 +108,10 @@ export default class BadgesModal extends React.PureComponent<Props, State> { | |||
type={selectedType} | |||
updateOptions={this.handleUpdateOptions} | |||
/> | |||
<CodeSnippet isOneLine={true} snippet={getBadgeUrl(selectedType, fullBadgeOptions)} /> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet={getBadgeSnippet(selectedType, fullBadgeOptions)} | |||
/> | |||
</div> | |||
<footer className="modal-foot"> | |||
<ResetButtonLink className="js-modal-close" onClick={this.handleClose}> |
@@ -54,8 +54,16 @@ it('should display measure badge params', () => { | |||
const updateOptions = jest.fn(); | |||
const wrapper = getWrapper({ updateOptions, type: BadgeType.measure }); | |||
expect(wrapper).toMatchSnapshot(); | |||
(wrapper.instance() as BadgeParams).handleColorChange({ value: 'black' }); | |||
expect(updateOptions).toHaveBeenCalledWith({ color: 'black' }); | |||
(wrapper.instance() as BadgeParams).handleMetricChange({ value: 'code_smell' }); | |||
expect(updateOptions).toHaveBeenCalledWith({ metric: 'code_smell' }); | |||
}); | |||
it('should display quality gate badge params', () => { | |||
const updateOptions = jest.fn(); | |||
const wrapper = getWrapper({ updateOptions, type: BadgeType.qualityGate }); | |||
expect(wrapper).toMatchSnapshot(); | |||
(wrapper.instance() as BadgeParams).handleFormatChange({ value: 'md' }); | |||
expect(updateOptions).toHaveBeenCalledWith({ format: 'md' }); | |||
}); | |||
function getWrapper(props = {}) { |
@@ -22,8 +22,13 @@ import { shallow } from 'enzyme'; | |||
import BadgesModal from '../BadgesModal'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
import { Location } from '../../../../helpers/urls'; | |||
jest.mock('../../../../helpers/urls', () => ({ getHostUrl: () => 'host' })); | |||
jest.mock('../../../../helpers/urls', () => ({ | |||
getHostUrl: () => 'host', | |||
getProjectUrl: () => ({ pathname: '/dashboard' } as Location), | |||
getPathUrlAsString: (l: Location) => l.pathname | |||
})); | |||
jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); | |||
const shortBranch: T.ShortLivingBranch = { | |||
@@ -33,7 +38,7 @@ const shortBranch: T.ShortLivingBranch = { | |||
type: 'SHORT' | |||
}; | |||
it('should display the modal after click on sonar cloud', () => { | |||
it('should display the modal after click on sonarcloud', () => { | |||
(isSonarCloud as jest.Mock).mockImplementation(() => true); | |||
const wrapper = shallow( | |||
<BadgesModal branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" /> | |||
@@ -43,7 +48,7 @@ it('should display the modal after click on sonar cloud', () => { | |||
expect(wrapper.find('Modal')).toMatchSnapshot(); | |||
}); | |||
it('should display the modal after click on sonar qube', () => { | |||
it('should display the modal after click on sonarqube', () => { | |||
(isSonarCloud as jest.Mock).mockImplementation(() => false); | |||
const wrapper = shallow( | |||
<BadgesModal branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" /> |
@@ -3,10 +3,11 @@ | |||
exports[`should display marketing badge params 1`] = ` | |||
<div> | |||
<label | |||
className="big-spacer-right" | |||
className="spacer-right" | |||
htmlFor="badge-color" | |||
> | |||
color | |||
: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
@@ -32,16 +33,44 @@ exports[`should display marketing badge params 1`] = ` | |||
searchable={false} | |||
value="white" | |||
/> | |||
<label | |||
className="spacer-right big-spacer-left" | |||
htmlFor="badge-format" | |||
> | |||
format | |||
: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
name="badge-format" | |||
onChange={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "overview.badges.options.formats.md", | |||
"value": "md", | |||
}, | |||
Object { | |||
"label": "overview.badges.options.formats.url", | |||
"value": "url", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="md" | |||
/> | |||
</div> | |||
`; | |||
exports[`should display measure badge params 1`] = ` | |||
<div> | |||
<label | |||
className="big-spacer-right" | |||
className="spacer-right" | |||
htmlFor="badge-metric" | |||
> | |||
overview.badges.metric | |||
: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
@@ -52,5 +81,64 @@ exports[`should display measure badge params 1`] = ` | |||
searchable={false} | |||
value="alert_status" | |||
/> | |||
<label | |||
className="spacer-right big-spacer-left" | |||
htmlFor="badge-format" | |||
> | |||
format | |||
: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
name="badge-format" | |||
onChange={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "overview.badges.options.formats.md", | |||
"value": "md", | |||
}, | |||
Object { | |||
"label": "overview.badges.options.formats.url", | |||
"value": "url", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="md" | |||
/> | |||
</div> | |||
`; | |||
exports[`should display quality gate badge params 1`] = ` | |||
<div> | |||
<label | |||
className="spacer-right" | |||
htmlFor="badge-format" | |||
> | |||
format | |||
: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
name="badge-format" | |||
onChange={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "overview.badges.options.formats.md", | |||
"value": "md", | |||
}, | |||
Object { | |||
"label": "overview.badges.options.formats.url", | |||
"value": "url", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="md" | |||
/> | |||
</div> | |||
`; |
@@ -1,6 +1,6 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display the modal after click on sonar cloud 1`] = ` | |||
exports[`should display the modal after click on sonarcloud 1`] = ` | |||
<div | |||
className="overview-meta-card" | |||
> | |||
@@ -13,7 +13,7 @@ exports[`should display the modal after click on sonar cloud 1`] = ` | |||
</div> | |||
`; | |||
exports[`should display the modal after click on sonar cloud 2`] = ` | |||
exports[`should display the modal after click on sonarcloud 2`] = ` | |||
<Modal | |||
contentLabel="overview.badges.title" | |||
onRequestClose={[Function]} | |||
@@ -77,7 +77,7 @@ exports[`should display the modal after click on sonar cloud 2`] = ` | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status" | |||
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status)](/dashboard)" | |||
/> | |||
</div> | |||
<footer | |||
@@ -93,7 +93,7 @@ exports[`should display the modal after click on sonar cloud 2`] = ` | |||
</Modal> | |||
`; | |||
exports[`should display the modal after click on sonar qube 1`] = ` | |||
exports[`should display the modal after click on sonarqube 1`] = ` | |||
<div | |||
className="overview-meta-card" | |||
> | |||
@@ -106,7 +106,7 @@ exports[`should display the modal after click on sonar qube 1`] = ` | |||
</div> | |||
`; | |||
exports[`should display the modal after click on sonar qube 2`] = ` | |||
exports[`should display the modal after click on sonarqube 2`] = ` | |||
<Modal | |||
contentLabel="overview.badges.title" | |||
onRequestClose={[Function]} | |||
@@ -163,7 +163,7 @@ exports[`should display the modal after click on sonar qube 2`] = ` | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status" | |||
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status)](/dashboard)" | |||
/> | |||
</div> | |||
<footer |
@@ -17,17 +17,21 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getBadgeUrl, BadgeOptions, BadgeType } from '../utils'; | |||
import { getBadgeUrl, BadgeOptions, BadgeType, getBadgeSnippet } from '../utils'; | |||
import { Location } from '../../../../helpers/urls'; | |||
jest.mock('../../../../helpers/urls', () => ({ | |||
getHostUrl: () => 'host' | |||
...require.requireActual('../../../../helpers/urls'), | |||
getHostUrl: () => 'host', | |||
getPathUrlAsString: (o: Location) => | |||
`host${o.pathname}?id=${o.query ? o.query.id : ''}&branch=${o.query ? o.query.branch : ''}` | |||
})); | |||
const options: BadgeOptions = { | |||
branch: 'master', | |||
color: 'white', | |||
project: 'foo', | |||
metric: 'alert_status' | |||
metric: 'alert_status', | |||
project: 'foo' | |||
}; | |||
describe('#getBadgeUrl', () => { | |||
@@ -41,7 +45,9 @@ describe('#getBadgeUrl', () => { | |||
}); | |||
it('should generate correct quality gate badge links', () => { | |||
expect(getBadgeUrl(BadgeType.qualityGate, options)); | |||
expect(getBadgeUrl(BadgeType.qualityGate, options)).toBe( | |||
'host/api/project_badges/quality_gate?branch=master&project=foo' | |||
); | |||
}); | |||
it('should generate correct measures badge links', () => { | |||
@@ -56,3 +62,11 @@ describe('#getBadgeUrl', () => { | |||
); | |||
}); | |||
}); | |||
describe('#getBadgeSnippet', () => { | |||
it('should generate a correct markdown image', () => { | |||
expect(getBadgeSnippet(BadgeType.marketing, { ...options, format: 'md' })).toBe( | |||
'[![SonarCloud](host/images/project_badges/sonarcloud-white.svg)](host/dashboard?id=foo&branch=master)' | |||
); | |||
}); | |||
}); |
@@ -19,13 +19,16 @@ | |||
*/ | |||
import { stringify } from 'querystring'; | |||
import { omitNil } from '../../../helpers/request'; | |||
import { getHostUrl } from '../../../helpers/urls'; | |||
import { getHostUrl, getProjectUrl, getPathUrlAsString } from '../../../helpers/urls'; | |||
import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
export type BadgeColors = 'white' | 'black' | 'orange'; | |||
export type BadgeFormats = 'md' | 'url'; | |||
export interface BadgeOptions { | |||
branch?: string; | |||
color?: BadgeColors; | |||
format?: BadgeFormats; | |||
project?: string; | |||
metric?: string; | |||
pullRequest?: string; | |||
@@ -37,6 +40,38 @@ export enum BadgeType { | |||
marketing = 'marketing' | |||
} | |||
export function getBadgeSnippet(type: BadgeType, options: BadgeOptions) { | |||
const url = getBadgeUrl(type, options); | |||
const { branch, format = 'md', metric = 'alert_status', project } = options; | |||
if (format === 'url') { | |||
return url; | |||
} else { | |||
let label; | |||
let projectUrl; | |||
switch (type) { | |||
case BadgeType.marketing: | |||
label = 'SonarCloud'; | |||
break; | |||
case BadgeType.measure: | |||
label = getLocalizedMetricName({ key: metric }); | |||
break; | |||
case BadgeType.qualityGate: | |||
default: | |||
label = 'Quality gate'; | |||
break; | |||
} | |||
if (project) { | |||
projectUrl = getPathUrlAsString(getProjectUrl(project, branch), false); | |||
} | |||
const mdImage = `![${label}](${url})`; | |||
return projectUrl ? `[${mdImage}](${projectUrl})` : mdImage; | |||
} | |||
} | |||
export function getBadgeUrl( | |||
type: BadgeType, | |||
{ branch, project, color = 'white', metric = 'alert_status', pullRequest }: BadgeOptions |
@@ -44,8 +44,10 @@ export function getHostUrl(): string { | |||
return window.location.origin + getBaseUrl(); | |||
} | |||
export function getPathUrlAsString(path: Location): string { | |||
return `${getBaseUrl()}${path.pathname}?${stringify(omitBy(path.query, isNil))}`; | |||
export function getPathUrlAsString(path: Location, internal = true): string { | |||
return `${internal ? getBaseUrl() : getHostUrl()}${path.pathname}?${stringify( | |||
omitBy(path.query, isNil) | |||
)}`; | |||
} | |||
export function getProjectUrl(project: string, branch?: string): Location { |
@@ -72,6 +72,7 @@ file=File | |||
files=Files | |||
filters=Filters | |||
follow=Follow | |||
format=Format | |||
from=From | |||
global=Global | |||
help=Help | |||
@@ -2501,6 +2502,8 @@ overview.badges.metric=Metric | |||
overview.badges.options.colors.white=White | |||
overview.badges.options.colors.black=Black | |||
overview.badges.options.colors.orange=Orange | |||
overview.badges.options.formats.md=Markdown | |||
overview.badges.options.formats.url=Image URL only | |||
overview.badges.measure.alt=Standard badge | |||
overview.badges.measure.description.TRK=This badge dynamically displays the current status of one metric of your project. | |||
overview.badges.measure.description.VW=This badge dynamically displays the current status of one metric of your portfolio. |