--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import throwGlobalError from '../app/utils/throwGlobalError';
+import { getJSON } from '../helpers/request';
+
+export function getProjectBadgesToken(project: string) {
+ return getJSON('/api/project_badges/token', { project })
+ .then(({ token }) => token)
+ .catch(throwGlobalError);
+}
isLoggedIn(currentUser) && component.qualifier === ComponentQualifier.Project;
const canUseBadges =
metrics !== undefined &&
- component.visibility !== 'private' &&
(component.qualifier === ComponentQualifier.Application ||
component.qualifier === ComponentQualifier.Project);
import { mockComponent } from '../../../../../../helpers/mocks/component';
import { mockCurrentUser, mockLoggedInUser, mockMetric } from '../../../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../../../helpers/testUtils';
+import { ComponentQualifier } from '../../../../../../types/component';
+import ProjectBadges from '../badges/ProjectBadges';
import { ProjectInformation } from '../ProjectInformation';
import { ProjectInformationPages } from '../ProjectInformationPages';
expect(wrapper.state().page).toBe(ProjectInformationPages.badges);
});
+it('should display badge', () => {
+ const wrapper = shallowRender({
+ component: mockComponent({ qualifier: ComponentQualifier.Project })
+ });
+
+ expect(wrapper.find(ProjectBadges).type).toBeDefined();
+
+ wrapper.setProps({ component: mockComponent({ qualifier: ComponentQualifier.Application }) });
+ expect(wrapper.find(ProjectBadges).type).toBeDefined();
+});
+
function shallowRender(props: Partial<ProjectInformation['props']> = {}) {
return shallow<ProjectInformation>(
<ProjectInformation
<Fragment>
<Memo(ProjectInformationRenderer)
canConfigureNotifications={false}
- canUseBadges={false}
+ canUseBadges={true}
component={
Object {
"breadcrumbs": Array [],
onComponentChange={[MockFunction]}
onPageChange={[Function]}
/>
+ <InfoDrawerPage
+ displayed={false}
+ onPageChange={[Function]}
+ >
+ <ProjectBadges
+ metrics={
+ Object {
+ "coverage": Object {
+ "id": "coverage",
+ "key": "coverage",
+ "name": "Coverage",
+ "type": "PERCENT",
+ },
+ }
+ }
+ project="my-project"
+ qualifier="TRK"
+ />
+ </InfoDrawerPage>
</Fragment>
`;
import { fetchWebApi } from '../../../../../../api/web-api';
import Select from '../../../../../../components/controls/Select';
import { getLocalizedMetricName, translate } from '../../../../../../helpers/l10n';
-import { BadgeColors, BadgeFormats, BadgeOptions, BadgeType } from './utils';
+import { BadgeFormats, BadgeOptions, BadgeType } from './utils';
interface Props {
className?: string;
});
};
- handleColorChange = ({ value }: { value: BadgeColors }) => {
- this.props.updateOptions({ color: value });
- };
-
handleFormatChange = ({ value }: { value: BadgeFormats }) => {
this.props.updateOptions({ format: 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) {
+ if (type === BadgeType.measure) {
return (
<>
<label className="spacer-right" htmlFor="badge-metric">
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { getProjectBadgesToken } from '../../../../../../api/project-badges';
import CodeSnippet from '../../../../../../components/common/CodeSnippet';
+import { Alert } from '../../../../../../components/ui/Alert';
import { getBranchLikeQuery } from '../../../../../../helpers/branch-like';
import { translate } from '../../../../../../helpers/l10n';
import { BranchLike } from '../../../../../../types/branch-like';
}
interface State {
+ token: string;
selectedType: BadgeType;
badgeOptions: BadgeOptions;
}
export default class ProjectBadges extends React.PureComponent<Props, State> {
+ mounted = false;
state: State = {
+ token: '',
selectedType: BadgeType.measure,
- badgeOptions: { color: 'white', metric: MetricKey.alert_status }
+ badgeOptions: { metric: MetricKey.alert_status }
};
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchToken();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ async fetchToken() {
+ const { project } = this.props;
+ const token = await getProjectBadgesToken(project);
+ if (this.mounted) {
+ this.setState({ token });
+ }
+ }
+
handleSelectBadge = (selectedType: BadgeType) => {
this.setState({ selectedType });
};
render() {
const { branchLike, project, qualifier } = this.props;
- const { selectedType, badgeOptions } = this.state;
+ const { selectedType, badgeOptions, token } = this.state;
const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) };
return (
onClick={this.handleSelectBadge}
selected={BadgeType.measure === selectedType}
type={BadgeType.measure}
- url={getBadgeUrl(BadgeType.measure, fullBadgeOptions)}
+ url={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token)}
/>
<p className="huge-spacer-bottom spacer-top">
{translate('overview.badges', BadgeType.measure, 'description', qualifier)}
onClick={this.handleSelectBadge}
selected={BadgeType.qualityGate === selectedType}
type={BadgeType.qualityGate}
- url={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions)}
+ url={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token)}
/>
<p className="huge-spacer-bottom spacer-top">
{translate('overview.badges', BadgeType.qualityGate, 'description', qualifier)}
type={selectedType}
updateOptions={this.handleUpdateOptions}
/>
- <CodeSnippet isOneLine={true} snippet={getBadgeSnippet(selectedType, fullBadgeOptions)} />
+ <Alert variant="warning">{translate('overview.badges.leak_warning')}</Alert>
+ <CodeSnippet
+ isOneLine={true}
+ snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)}
+ />
</div>
);
}
const onClick = jest.fn();
const wrapper = getWrapper({ onClick });
click(wrapper.find('Button'));
- expect(onClick).toHaveBeenCalledWith(BadgeType.marketing);
+ expect(onClick).toHaveBeenCalledWith(BadgeType.qualityGate);
});
function getWrapper(props = {}) {
<BadgeButton
onClick={jest.fn()}
selected={false}
- type={BadgeType.marketing}
+ type={BadgeType.qualityGate}
url="http://foo.bar"
{...props}
/>
coverage: { key: 'coverage', name: 'Coverage' } as T.Metric
};
-it('should display marketing badge params', () => {
- const updateOptions = jest.fn();
- const wrapper = getWrapper({ updateOptions });
- expect(wrapper).toMatchSnapshot();
- (wrapper.instance() as BadgeParams).handleColorChange({ value: 'black' });
- expect(updateOptions).toHaveBeenCalledWith({ color: 'black' });
-});
-
it('should display measure badge params', () => {
const updateOptions = jest.fn();
const wrapper = getWrapper({ updateOptions, type: BadgeType.measure });
return shallow(
<BadgeParams
metrics={METRICS}
- options={{ color: 'white', metric: 'alert_status' }}
- type={BadgeType.marketing}
+ options={{ metric: 'alert_status' }}
+ type={BadgeType.measure}
updateOptions={jest.fn()}
{...props}
/>
import * as React from 'react';
import { mockBranch } from '../../../../../../../helpers/mocks/branch-like';
import { mockMetric } from '../../../../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../../../../helpers/testUtils';
import { Location } from '../../../../../../../helpers/urls';
import { MetricKey } from '../../../../../../../types/metrics';
import ProjectBadges from '../ProjectBadges';
getProjectUrl: () => ({ pathname: '/dashboard' } as Location)
}));
-it('should display correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
+jest.mock('../../../../../../../api/project-badges', () => ({
+ getProjectBadgesToken: jest.fn().mockResolvedValue('foo')
+}));
+
+it('should display correctly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
});
function shallowRender(overrides = {}) {
onClick={[Function]}
>
<img
- alt="overview.badges.marketing.alt"
+ alt="overview.badges.quality_gate.alt"
src="http://foo.bar"
width="128px"
/>
onClick={[Function]}
>
<img
- alt="overview.badges.marketing.alt"
+ alt="overview.badges.quality_gate.alt"
src="http://foo.bar"
width="128px"
/>
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should display marketing badge params 1`] = `
-<div>
- <label
- className="spacer-right"
- htmlFor="badge-color"
- >
- color
- :
- </label>
- <Select
- className="input-medium"
- clearable={false}
- name="badge-color"
- onChange={[Function]}
- options={
- Array [
- Object {
- "label": "overview.badges.options.colors.white",
- "value": "white",
- },
- Object {
- "label": "overview.badges.options.colors.black",
- "value": "black",
- },
- Object {
- "label": "overview.badges.options.colors.orange",
- "value": "orange",
- },
- ]
- }
- searchable={false}
- value="white"
- />
- <label
- className="spacer-right spacer-top"
- 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
onClick={[Function]}
selected={true}
type="measure"
- url="host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status"
+ url="host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status&token=foo"
/>
<p
className="huge-spacer-bottom spacer-top"
onClick={[Function]}
selected={false}
type="quality_gate"
- url="host/api/project_badges/quality_gate?branch=branch-6.7&project=foo"
+ url="host/api/project_badges/quality_gate?branch=branch-6.7&project=foo&token=foo"
/>
<p
className="huge-spacer-bottom spacer-top"
}
options={
Object {
- "color": "white",
"metric": "alert_status",
}
}
type="measure"
updateOptions={[Function]}
/>
+ <Alert
+ variant="warning"
+ >
+ overview.badges.leak_warning
+ </Alert>
<CodeSnippet
isOneLine={true}
- snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status)](/dashboard)"
+ snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status&token=foo)](/dashboard)"
/>
</div>
`;
const options: BadgeOptions = {
branch: 'master',
- color: 'white',
metric: 'alert_status',
project: 'foo'
};
describe('#getBadgeUrl', () => {
- it('should generate correct marketing badge links', () => {
- expect(getBadgeUrl(BadgeType.marketing, options)).toBe(
- 'host/images/project_badges/sonarcloud-white.svg'
- );
- expect(getBadgeUrl(BadgeType.marketing, { ...options, color: 'orange' })).toBe(
- 'host/images/project_badges/sonarcloud-orange.svg'
- );
- });
-
it('should generate correct quality gate badge links', () => {
- expect(getBadgeUrl(BadgeType.qualityGate, options)).toBe(
- 'host/api/project_badges/quality_gate?branch=master&project=foo'
+ expect(getBadgeUrl(BadgeType.qualityGate, options, 'foo')).toBe(
+ 'host/api/project_badges/quality_gate?branch=master&project=foo&token=foo'
);
});
it('should generate correct measures badge links', () => {
- expect(getBadgeUrl(BadgeType.measure, options)).toBe(
- 'host/api/project_badges/measure?branch=master&project=foo&metric=alert_status'
+ expect(getBadgeUrl(BadgeType.measure, options, 'foo')).toBe(
+ 'host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo'
);
});
it('should ignore undefined parameters', () => {
- expect(getBadgeUrl(BadgeType.measure, { color: 'white', metric: 'alert_status' })).toBe(
- 'host/api/project_badges/measure?metric=alert_status'
+ expect(getBadgeUrl(BadgeType.measure, { metric: 'alert_status' }, 'foo')).toBe(
+ 'host/api/project_badges/measure?metric=alert_status&token=foo'
+ );
+ });
+
+ it('should force metric parameters', () => {
+ expect(getBadgeUrl(BadgeType.measure, {}, 'foo')).toBe(
+ 'host/api/project_badges/measure?metric=alert_status&token=foo'
);
});
});
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)'
+ expect(getBadgeSnippet(BadgeType.measure, { ...options, format: 'md' }, 'foo')).toBe(
+ '[![alert_status](host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo)](host/dashboard?id=foo&branch=master)'
);
});
});
export interface BadgeOptions {
branch?: string;
- color?: BadgeColors;
format?: BadgeFormats;
project?: string;
metric?: string;
export enum BadgeType {
measure = 'measure',
- qualityGate = 'quality_gate',
- marketing = 'marketing'
+ qualityGate = 'quality_gate'
}
-export function getBadgeSnippet(type: BadgeType, options: BadgeOptions) {
- const url = getBadgeUrl(type, options);
+export function getBadgeSnippet(type: BadgeType, options: BadgeOptions, token: string) {
+ const url = getBadgeUrl(type, options, token);
const { branch, format = 'md', metric = 'alert_status', project } = options;
if (format === 'url') {
let projectUrl;
switch (type) {
- case BadgeType.marketing:
- label = 'SonarCloud';
- break;
case BadgeType.measure:
label = getLocalizedMetricName({ key: metric });
break;
export function getBadgeUrl(
type: BadgeType,
- { branch, project, color = 'white', metric = 'alert_status', pullRequest }: BadgeOptions
+ { branch, project, metric = 'alert_status', pullRequest }: BadgeOptions,
+ token: string
) {
switch (type) {
- case BadgeType.marketing:
- return `${getHostUrl()}/images/project_badges/sonarcloud-${color}.svg`;
case BadgeType.qualityGate:
return `${getHostUrl()}/api/project_badges/quality_gate?${new URLSearchParams(
- omitNil({ branch, project, pullRequest })
+ omitNil({ branch, project, pullRequest, token })
).toString()}`;
case BadgeType.measure:
default:
return `${getHostUrl()}/api/project_badges/measure?${new URLSearchParams(
- omitNil({ branch, project, metric, pullRequest })
+ omitNil({ branch, project, metric, pullRequest, token })
).toString()}`;
}
}
return (
<div className={classNames('code-snippet spacer-top spacer-bottom display-flex-row', {})}>
- <pre className="flex-1" ref={snippetRef}>
+ {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
+ <pre className="flex-1" ref={snippetRef} tabIndex={0}>
{finalSnippet}
</pre>
{!noCopy && <ClipboardButton copyValue={finalSnippet} />}
>
<pre
className="flex-1"
+ tabIndex={0}
>
foo \\
bar
>
<pre
className="flex-1"
+ tabIndex={0}
>
foo
bar
>
<pre
className="flex-1"
+ tabIndex={0}
>
foo
bar
>
<pre
className="flex-1"
+ tabIndex={0}
>
foo bar
</pre>
overview.badges.quality_gate.description.APP=Displays the current quality gate status of your application.
overview.badges.quality_gate.description.TRK=Displays the current quality gate status of your project.
overview.badges.quality_gate.description.VW=Displays the current quality gate status of your portfolio.
-
+overview.badges.leak_warning=Project badges can expose your security rating and other measures. Only use project badges in trusted environments.
#------------------------------------------------------------------------------
#