diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2018-12-24 12:35:10 +0100 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-12-26 20:20:58 +0100 |
commit | ea6102e599b37a73cce37675bff9456338c1f75f (patch) | |
tree | 42092c04ebe4f44c8cd646477f5a40ddc34ab9b9 | |
parent | a9fd4eb48b2ae984bdcac4e2e8ebdd16d5ac3a76 (diff) | |
download | sonarqube-ea6102e599b37a73cce37675bff9456338c1f75f.tar.gz sonarqube-ea6102e599b37a73cce37675bff9456338c1f75f.zip |
SONAR-10649 Add Markdown format for badges
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. |