]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10649 Add Markdown format for badges
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Mon, 24 Dec 2018 11:35:10 +0000 (12:35 +0100)
committerSonarTech <sonartech@sonarsource.com>
Wed, 26 Dec 2018 19:20:58 +0000 (20:20 +0100)
server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx
server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx
server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx
server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx
server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/badges/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/overview/badges/utils.ts
server/sonar-web/src/main/js/helpers/urls.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index ddb7de1f365d3fc409cb6dec83f427e2664d6716..5a0041ef10633b0bf6c1574410c82b96618cbeab 100644 (file)
@@ -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>
+    );
   }
 }
index 1962e6f73f09f96dfe823a2b6a26be0ceaa87eb0..49a50db7f068140a63e0b4d144d773a736aa525b 100644 (file)
@@ -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}>
index 7656a4ac6290da78678987cd1d2c3731b407835f..099cf6590b520093f7bd2048a1dc6ac07314ea79 100644 (file)
@@ -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 = {}) {
index eedcb7913f02634036012f4667541b5890333593..393098ecafca17d6dda254ebeab569b5f3a5079a 100644 (file)
@@ -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" />
index 58bb0b507a672cd0764a764bdbc179a9b20d55b7..ca977adcec21efb10a34a3419f7f328f0b45610c 100644 (file)
@@ -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>
 `;
index 012ed7da82100b0f91a44518af7c5f8609795a98..43a0e7f7638dacbaa974295ef8f259cc1f07fc12 100644 (file)
@@ -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
index 59d643811b9011a6625f0711e8fe1e7092261229..526c4df131a4452940ae0f023d8f5eeb9c6b3ce2 100644 (file)
  * 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)'
+    );
+  });
+});
index 5f300830c1bf1977678957d6a7353011b381b3a5..a08806e05f6346d3bade1838280b5cd1a38463b0 100644 (file)
  */
 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
index c8c7db886a59813723bc1f9e0e956e242496798f..ff94ab0fee8f1e94c5843d7761a02ad78951d75e 100644 (file)
@@ -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 {
index 5d9e42082a1919bc1afcc8bfd767e502e0a49bcb..180db4334de75240e4f1e049807e0684a154486c 100644 (file)
@@ -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.