aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2018-12-24 12:35:10 +0100
committerSonarTech <sonartech@sonarsource.com>2018-12-26 20:20:58 +0100
commitea6102e599b37a73cce37675bff9456338c1f75f (patch)
tree42092c04ebe4f44c8cd646477f5a40ddc34ab9b9
parenta9fd4eb48b2ae984bdcac4e2e8ebdd16d5ac3a76 (diff)
downloadsonarqube-ea6102e599b37a73cce37675bff9456338c1f75f.tar.gz
sonarqube-ea6102e599b37a73cce37675bff9456338c1f75f.zip
SONAR-10649 Add Markdown format for badges
-rw-r--r--server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx130
-rw-r--r--server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap92
-rw-r--r--server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/badges/__tests__/utils-test.ts24
-rw-r--r--server/sonar-web/src/main/js/apps/overview/badges/utils.ts37
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.ts6
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties3
10 files changed, 266 insertions, 68 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx b/server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx
index ddb7de1f365..5a0041ef106 100644
--- a/server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx
@@ -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>
+ );
}
}
diff --git a/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx b/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx
index 1962e6f73f0..49a50db7f06 100644
--- a/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx
@@ -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}>
diff --git a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx
index 7656a4ac629..099cf6590b5 100644
--- a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx
@@ -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 = {}) {
diff --git a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx
index eedcb7913f0..393098ecafc 100644
--- a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx
@@ -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" />
diff --git a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap
index 58bb0b507a6..ca977adcec2 100644
--- a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap
@@ -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>
`;
diff --git a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap
index 012ed7da821..43a0e7f7638 100644
--- a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap
@@ -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
diff --git a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/utils-test.ts
index 59d643811b9..526c4df131a 100644
--- a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/utils-test.ts
@@ -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)'
+ );
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/badges/utils.ts b/server/sonar-web/src/main/js/apps/overview/badges/utils.ts
index 5f300830c1b..a08806e05f6 100644
--- a/server/sonar-web/src/main/js/apps/overview/badges/utils.ts
+++ b/server/sonar-web/src/main/js/apps/overview/badges/utils.ts
@@ -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
diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts
index c8c7db886a5..ff94ab0fee8 100644
--- a/server/sonar-web/src/main/js/helpers/urls.ts
+++ b/server/sonar-web/src/main/js/helpers/urls.ts
@@ -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 {
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 5d9e42082a1..180db4334de 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -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.