Browse Source

SONAR-10649 Add Markdown format for badges

tags/7.6
Wouter Admiraal 5 years ago
parent
commit
ea6102e599

+ 85
- 45
server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx View File

* 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>
);
} }
} }

+ 5
- 2
server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx View File

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

+ 10
- 2
server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx View File

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 = {}) {

+ 8
- 3
server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx View File

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

+ 90
- 2
server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap View File

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

+ 6
- 6
server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap View File

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

+ 19
- 5
server/sonar-web/src/main/js/apps/overview/badges/__tests__/utils-test.ts View File

* 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)'
);
});
});

+ 36
- 1
server/sonar-web/src/main/js/apps/overview/badges/utils.ts View File

*/ */
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

+ 4
- 2
server/sonar-web/src/main/js/helpers/urls.ts View File

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 {

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

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.

Loading…
Cancel
Save