* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | 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 Select from '../../../components/controls/Select'; | ||||
import { fetchWebApi } from '../../../api/web-api'; | import { fetchWebApi } from '../../../api/web-api'; | ||||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | ||||
); | ); | ||||
} | } | ||||
getColorOptions = () => | |||||
['white', 'black', 'orange'].map(color => ({ | |||||
getColorOptions = () => { | |||||
return ['white', 'black', 'orange'].map(color => ({ | |||||
label: translate('overview.badges.options.colors', color), | label: translate('overview.badges.options.colors', color), | ||||
value: 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]; | const metric = this.props.metrics[key]; | ||||
return { | return { | ||||
value: key, | value: key, | ||||
label: metric ? getLocalizedMetricName(metric) : key | label: metric ? getLocalizedMetricName(metric) : key | ||||
}; | }; | ||||
}); | }); | ||||
}; | |||||
handleColorChange = ({ value }: { value: BadgeColors }) => | |||||
handleColorChange = ({ value }: { value: BadgeColors }) => { | |||||
this.props.updateOptions({ color: value }); | 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 }); | 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() { | render() { | ||||
const { className, options, type } = this.props; | 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> | |||||
); | |||||
} | } | ||||
} | } |
import * as React from 'react'; | import * as React from 'react'; | ||||
import BadgeButton from './BadgeButton'; | import BadgeButton from './BadgeButton'; | ||||
import BadgeParams from './BadgeParams'; | import BadgeParams from './BadgeParams'; | ||||
import { BadgeType, BadgeOptions, getBadgeUrl } from './utils'; | |||||
import { BadgeType, BadgeOptions, getBadgeUrl, getBadgeSnippet } from './utils'; | |||||
import CodeSnippet from '../../../components/common/CodeSnippet'; | import CodeSnippet from '../../../components/common/CodeSnippet'; | ||||
import Modal from '../../../components/controls/Modal'; | import Modal from '../../../components/controls/Modal'; | ||||
import { getBranchLikeQuery } from '../../../helpers/branches'; | import { getBranchLikeQuery } from '../../../helpers/branches'; | ||||
type={selectedType} | type={selectedType} | ||||
updateOptions={this.handleUpdateOptions} | updateOptions={this.handleUpdateOptions} | ||||
/> | /> | ||||
<CodeSnippet isOneLine={true} snippet={getBadgeUrl(selectedType, fullBadgeOptions)} /> | |||||
<CodeSnippet | |||||
isOneLine={true} | |||||
snippet={getBadgeSnippet(selectedType, fullBadgeOptions)} | |||||
/> | |||||
</div> | </div> | ||||
<footer className="modal-foot"> | <footer className="modal-foot"> | ||||
<ResetButtonLink className="js-modal-close" onClick={this.handleClose}> | <ResetButtonLink className="js-modal-close" onClick={this.handleClose}> |
const updateOptions = jest.fn(); | const updateOptions = jest.fn(); | ||||
const wrapper = getWrapper({ updateOptions, type: BadgeType.measure }); | const wrapper = getWrapper({ updateOptions, type: BadgeType.measure }); | ||||
expect(wrapper).toMatchSnapshot(); | 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 = {}) { | function getWrapper(props = {}) { |
import BadgesModal from '../BadgesModal'; | import BadgesModal from '../BadgesModal'; | ||||
import { click } from '../../../../helpers/testUtils'; | import { click } from '../../../../helpers/testUtils'; | ||||
import { isSonarCloud } from '../../../../helpers/system'; | 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() })); | jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); | ||||
const shortBranch: T.ShortLivingBranch = { | const shortBranch: T.ShortLivingBranch = { | ||||
type: 'SHORT' | 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); | (isSonarCloud as jest.Mock).mockImplementation(() => true); | ||||
const wrapper = shallow( | const wrapper = shallow( | ||||
<BadgesModal branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" /> | <BadgesModal branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" /> | ||||
expect(wrapper.find('Modal')).toMatchSnapshot(); | 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); | (isSonarCloud as jest.Mock).mockImplementation(() => false); | ||||
const wrapper = shallow( | const wrapper = shallow( | ||||
<BadgesModal branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" /> | <BadgesModal branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" /> |
exports[`should display marketing badge params 1`] = ` | exports[`should display marketing badge params 1`] = ` | ||||
<div> | <div> | ||||
<label | <label | ||||
className="big-spacer-right" | |||||
className="spacer-right" | |||||
htmlFor="badge-color" | htmlFor="badge-color" | ||||
> | > | ||||
color | color | ||||
: | |||||
</label> | </label> | ||||
<Select | <Select | ||||
className="input-medium" | className="input-medium" | ||||
searchable={false} | searchable={false} | ||||
value="white" | 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> | </div> | ||||
`; | `; | ||||
exports[`should display measure badge params 1`] = ` | exports[`should display measure badge params 1`] = ` | ||||
<div> | <div> | ||||
<label | <label | ||||
className="big-spacer-right" | |||||
className="spacer-right" | |||||
htmlFor="badge-metric" | htmlFor="badge-metric" | ||||
> | > | ||||
overview.badges.metric | overview.badges.metric | ||||
: | |||||
</label> | </label> | ||||
<Select | <Select | ||||
className="input-medium" | className="input-medium" | ||||
searchable={false} | searchable={false} | ||||
value="alert_status" | 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> | </div> | ||||
`; | `; |
// Jest Snapshot v1, https://goo.gl/fbAQLP | // 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 | <div | ||||
className="overview-meta-card" | className="overview-meta-card" | ||||
> | > | ||||
</div> | </div> | ||||
`; | `; | ||||
exports[`should display the modal after click on sonar cloud 2`] = ` | |||||
exports[`should display the modal after click on sonarcloud 2`] = ` | |||||
<Modal | <Modal | ||||
contentLabel="overview.badges.title" | contentLabel="overview.badges.title" | ||||
onRequestClose={[Function]} | onRequestClose={[Function]} | ||||
/> | /> | ||||
<CodeSnippet | <CodeSnippet | ||||
isOneLine={true} | 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> | </div> | ||||
<footer | <footer | ||||
</Modal> | </Modal> | ||||
`; | `; | ||||
exports[`should display the modal after click on sonar qube 1`] = ` | |||||
exports[`should display the modal after click on sonarqube 1`] = ` | |||||
<div | <div | ||||
className="overview-meta-card" | className="overview-meta-card" | ||||
> | > | ||||
</div> | </div> | ||||
`; | `; | ||||
exports[`should display the modal after click on sonar qube 2`] = ` | |||||
exports[`should display the modal after click on sonarqube 2`] = ` | |||||
<Modal | <Modal | ||||
contentLabel="overview.badges.title" | contentLabel="overview.badges.title" | ||||
onRequestClose={[Function]} | onRequestClose={[Function]} | ||||
/> | /> | ||||
<CodeSnippet | <CodeSnippet | ||||
isOneLine={true} | 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> | </div> | ||||
<footer | <footer |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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', () => ({ | 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 = { | const options: BadgeOptions = { | ||||
branch: 'master', | branch: 'master', | ||||
color: 'white', | color: 'white', | ||||
project: 'foo', | |||||
metric: 'alert_status' | |||||
metric: 'alert_status', | |||||
project: 'foo' | |||||
}; | }; | ||||
describe('#getBadgeUrl', () => { | describe('#getBadgeUrl', () => { | ||||
}); | }); | ||||
it('should generate correct quality gate badge links', () => { | 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', () => { | it('should generate correct measures badge links', () => { | ||||
); | ); | ||||
}); | }); | ||||
}); | }); | ||||
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)' | |||||
); | |||||
}); | |||||
}); |
*/ | */ | ||||
import { stringify } from 'querystring'; | import { stringify } from 'querystring'; | ||||
import { omitNil } from '../../../helpers/request'; | 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 BadgeColors = 'white' | 'black' | 'orange'; | ||||
export type BadgeFormats = 'md' | 'url'; | |||||
export interface BadgeOptions { | export interface BadgeOptions { | ||||
branch?: string; | branch?: string; | ||||
color?: BadgeColors; | color?: BadgeColors; | ||||
format?: BadgeFormats; | |||||
project?: string; | project?: string; | ||||
metric?: string; | metric?: string; | ||||
pullRequest?: string; | pullRequest?: string; | ||||
marketing = 'marketing' | 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( | export function getBadgeUrl( | ||||
type: BadgeType, | type: BadgeType, | ||||
{ branch, project, color = 'white', metric = 'alert_status', pullRequest }: BadgeOptions | { branch, project, color = 'white', metric = 'alert_status', pullRequest }: BadgeOptions |
return window.location.origin + getBaseUrl(); | 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 { | export function getProjectUrl(project: string, branch?: string): Location { |
files=Files | files=Files | ||||
filters=Filters | filters=Filters | ||||
follow=Follow | follow=Follow | ||||
format=Format | |||||
from=From | from=From | ||||
global=Global | global=Global | ||||
help=Help | help=Help | ||||
overview.badges.options.colors.white=White | overview.badges.options.colors.white=White | ||||
overview.badges.options.colors.black=Black | overview.badges.options.colors.black=Black | ||||
overview.badges.options.colors.orange=Orange | 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.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.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. | overview.badges.measure.description.VW=This badge dynamically displays the current status of one metric of your portfolio. |