aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-08-23 12:54:35 +0200
committersonartech <sonartech@sonarsource.com>2018-09-19 10:51:41 +0200
commit26e6aab3da5364bcb31fa8b23766610464a8781c (patch)
tree9902b310d48b56faed784e8a9a1a17e8ad2af782 /server
parentbc4d2e6948669842ff325c789f570a4f46f0af41 (diff)
downloadsonarqube-26e6aab3da5364bcb31fa8b23766610464a8781c.tar.gz
sonarqube-26e6aab3da5364bcb31fa8b23766610464a8781c.zip
SONAR-11159 SONAR-11160 Add new coverage and overall coverage next to branch status
Diffstat (limited to 'server')
-rw-r--r--server/sonar-vsts/src/main/js/components/QGWidget.tsx7
-rw-r--r--server/sonar-vsts/src/main/js/components/Widget.tsx9
-rw-r--r--server/sonar-web/src/main/js/api/measures.ts15
-rw-r--r--server/sonar-web/src/main/js/app/components/ComponentContainer.tsx114
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx45
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx9
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx16
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap29
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css16
-rw-r--r--server/sonar-web/src/main/js/app/types.ts1
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.tsx.snap8
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/style.css18
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/common/BranchMeasures.tsx76
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/BranchMeasures-test.tsx51
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchMeasures-test.tsx.snap93
-rw-r--r--server/sonar-web/src/main/js/components/nav/ContextNavBar.css2
-rw-r--r--server/sonar-web/src/main/js/components/ui/CoverageRating.tsx6
24 files changed, 454 insertions, 97 deletions
diff --git a/server/sonar-vsts/src/main/js/components/QGWidget.tsx b/server/sonar-vsts/src/main/js/components/QGWidget.tsx
index e4ecb754992..330ead0ec99 100644
--- a/server/sonar-vsts/src/main/js/components/QGWidget.tsx
+++ b/server/sonar-vsts/src/main/js/components/QGWidget.tsx
@@ -21,14 +21,14 @@ import * as React from 'react';
import * as classNames from 'classnames';
import SonarCloudIcon from './SonarCloudIcon';
import Tooltip from '../../../../../sonar-web/src/main/js/components/controls/Tooltip';
-import { MeasureComponent } from '../../../../../sonar-web/src/main/js/api/measures';
import {
getPathUrlAsString,
getProjectUrl
} from '../../../../../sonar-web/src/main/js/helpers/urls';
+import { ComponentMeasure } from '../../../../../sonar-web/src/main/js/app/types';
interface Props {
- component: MeasureComponent;
+ component: ComponentMeasure;
}
const QG_LEVELS: { [level: string]: string } = {
@@ -39,7 +39,8 @@ const QG_LEVELS: { [level: string]: string } = {
};
export default function QGWidget({ component }: Props) {
- const qgMeasure = component && component.measures.find(m => m.metric === 'alert_status');
+ const qgMeasure =
+ component && component.measures && component.measures.find(m => m.metric === 'alert_status');
if (!qgMeasure || !qgMeasure.value) {
return <p>Project Quality Gate not computed.</p>;
diff --git a/server/sonar-vsts/src/main/js/components/Widget.tsx b/server/sonar-vsts/src/main/js/components/Widget.tsx
index 03655b0ccf5..049d8c45f9d 100644
--- a/server/sonar-vsts/src/main/js/components/Widget.tsx
+++ b/server/sonar-vsts/src/main/js/components/Widget.tsx
@@ -20,11 +20,8 @@
import * as React from 'react';
import QGWidget from './QGWidget';
import LoginForm from './LoginForm';
-import {
- getMeasuresAndMeta,
- MeasureComponent
-} from '../../../../../sonar-web/src/main/js/api/measures';
-import { Metric } from '../../../../../sonar-web/src/main/js/app/types';
+import { getMeasuresAndMeta } from '../../../../../sonar-web/src/main/js/api/measures';
+import { Metric, ComponentMeasure } from '../../../../../sonar-web/src/main/js/app/types';
import { Settings } from '../utils';
interface Props {
@@ -32,7 +29,7 @@ interface Props {
}
interface State {
- component?: MeasureComponent;
+ component?: ComponentMeasure;
loading: boolean;
metrics?: Metric[];
unauthorized: boolean;
diff --git a/server/sonar-web/src/main/js/api/measures.ts b/server/sonar-web/src/main/js/api/measures.ts
index 378c5079242..9a5357a174a 100644
--- a/server/sonar-web/src/main/js/api/measures.ts
+++ b/server/sonar-web/src/main/js/api/measures.ts
@@ -20,8 +20,9 @@
import { getJSON, RequestData, postJSON, post } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
import {
- CustomMeasure,
BranchParameters,
+ ComponentMeasure,
+ CustomMeasure,
Measure,
Metric,
Paging,
@@ -31,23 +32,15 @@ import {
export function getMeasures(
data: { componentKey: string; metricKeys: string } & BranchParameters
-): Promise<{ metric: string; value?: string }[]> {
+): Promise<Measure[]> {
return getJSON('/api/measures/component', data).then(r => r.component.measures, throwGlobalError);
}
-export interface MeasureComponent {
- key: string;
- description?: string;
- measures: Measure[];
- name: string;
- qualifier: string;
-}
-
export function getMeasuresAndMeta(
componentKey: string,
metrics: string[],
additional: RequestData = {}
-): Promise<{ component: MeasureComponent; metrics?: Metric[]; periods?: Period[] }> {
+): Promise<{ component: ComponentMeasure; metrics?: Metric[]; periods?: Period[] }> {
const data = { ...additional, componentKey, metricKeys: metrics.join(',') };
return getJSON('/api/measures/component', data);
}
diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
index 98a8b23ad79..00bc81854f5 100644
--- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
@@ -23,11 +23,12 @@ import { connect } from 'react-redux';
import { differenceBy } from 'lodash';
import ComponentContainerNotFound from './ComponentContainerNotFound';
import ComponentNav from './nav/component/ComponentNav';
-import { Component, BranchLike } from '../types';
+import { Component, BranchLike, Measure } from '../types';
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
import { getBranches, getPullRequests } from '../../api/branches';
import { Task, getTasksForComponent, PendingTask } from '../../api/ce';
import { getComponentData } from '../../api/components';
+import { getMeasures } from '../../api/measures';
import { getComponentNavigation } from '../../api/nav';
import { fetchOrganizations } from '../../store/rootActions';
import { STATUSES } from '../../apps/background-tasks/constants';
@@ -36,7 +37,8 @@ import {
isBranch,
isMainBranch,
isLongLivingBranch,
- isShortLivingBranch
+ isShortLivingBranch,
+ getBranchLikeQuery
} from '../../helpers/branches';
interface Props {
@@ -50,6 +52,7 @@ interface Props {
interface State {
branchLike?: BranchLike;
branchLikes: BranchLike[];
+ branchMeasures?: Measure[];
component?: Component;
currentTask?: Task;
isPending: boolean;
@@ -114,46 +117,88 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
Promise.all([
getComponentNavigation({ componentKey: key, branch, pullRequest }),
getComponentData({ component: key, branch, pullRequest })
- ]).then(([nav, data]) => {
- const component = this.addQualifier({ ...nav, ...data });
+ ])
+ .then(([nav, data]) => {
+ const component = this.addQualifier({ ...nav, ...data });
- if (this.context.organizationsEnabled) {
- this.props.fetchOrganizations([component.organization]);
- }
-
- this.fetchBranches(component).then(({ branchLike, branchLikes }) => {
+ if (this.context.organizationsEnabled) {
+ this.props.fetchOrganizations([component.organization]);
+ }
+ return component;
+ })
+ .then(this.fetchBranches)
+ .then(this.fetchBranchMeasures)
+ .then(({ branchLike, branchLikes, component, branchMeasures }) => {
if (this.mounted) {
- this.setState({ branchLike, branchLikes, component, loading: false });
+ this.setState({
+ branchLike,
+ branchLikes,
+ branchMeasures,
+ component,
+ loading: false
+ });
this.fetchStatus(component);
}
- }, onError);
- }, onError);
+ })
+ .catch(onError);
}
fetchBranches = (
component: Component
- ): Promise<{ branchLike?: BranchLike; branchLikes: BranchLike[] }> => {
+ ): Promise<{
+ branchLike?: BranchLike;
+ branchLikes: BranchLike[];
+ component: Component;
+ }> => {
const application = component.breadcrumbs.find(({ qualifier }) => qualifier === 'APP');
if (application) {
return getBranches(application.key).then(branchLikes => {
return {
branchLike: this.getCurrentBranchLike(branchLikes),
- branchLikes
+ branchLikes,
+ component
};
});
}
const project = component.breadcrumbs.find(({ qualifier }) => qualifier === 'TRK');
- return project
- ? Promise.all([getBranches(project.key), getPullRequests(project.key)]).then(
- ([branches, pullRequests]) => {
- const branchLikes = [...branches, ...pullRequests];
- return {
- branchLike: this.getCurrentBranchLike(branchLikes),
- branchLikes
- };
- }
- )
- : Promise.resolve({ branchLikes: [] });
+ if (project) {
+ return Promise.all([getBranches(project.key), getPullRequests(project.key)]).then(
+ ([branches, pullRequests]) => {
+ const branchLikes = [...branches, ...pullRequests];
+ const branchLike = this.getCurrentBranchLike(branchLikes);
+ return { branchLike, branchLikes, component };
+ }
+ );
+ }
+
+ return Promise.resolve({ branchLikes: [], component });
+ };
+
+ fetchBranchMeasures = ({
+ branchLike,
+ branchLikes,
+ component
+ }: {
+ branchLike: BranchLike;
+ branchLikes: BranchLike[];
+ component: Component;
+ }): Promise<{
+ branchLike?: BranchLike;
+ branchLikes: BranchLike[];
+ branchMeasures?: Measure[];
+ component: Component;
+ }> => {
+ const project = component.breadcrumbs.find(({ qualifier }) => qualifier === 'TRK');
+ if (project && (isShortLivingBranch(branchLike) || isPullRequest(branchLike))) {
+ return getMeasures({
+ componentKey: project.key,
+ metricKeys: 'coverage,new_coverage',
+ ...getBranchLikeQuery(branchLike)
+ }).then(measures => {
+ return { branchLike, branchLikes, branchMeasures: measures, component };
+ });
+ }
+ return Promise.resolve({ branchLike, branchLikes, component });
};
fetchStatus = (component: Component) => {
@@ -259,14 +304,16 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
handleBranchesChange = () => {
if (this.mounted && this.state.component) {
- this.fetchBranches(this.state.component).then(
- ({ branchLike, branchLikes }) => {
- if (this.mounted) {
- this.setState({ branchLike, branchLikes });
- }
- },
- () => {}
- );
+ this.fetchBranches(this.state.component)
+ .then(this.fetchBranchMeasures)
+ .then(
+ ({ branchLike, branchLikes, branchMeasures }) => {
+ if (this.mounted) {
+ this.setState({ branchLike, branchLikes, branchMeasures });
+ }
+ },
+ () => {}
+ );
}
};
@@ -286,6 +333,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
!['FIL', 'UTS'].includes(component.qualifier) && (
<ComponentNav
branchLikes={branchLikes}
+ branchMeasures={this.state.branchMeasures}
component={component}
currentBranchLike={branchLike}
currentTask={currentTask}
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
index e67564559f6..18a71ecb3e8 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
@@ -34,6 +34,7 @@ import {
} from '../../types';
import { STATUSES } from '../../../apps/background-tasks/constants';
import { waitAndUpdate } from '../../../helpers/testUtils';
+import { getMeasures } from '../../../api/measures';
jest.mock('../../../api/branches', () => ({
getBranches: jest.fn().mockResolvedValue([]),
@@ -48,6 +49,15 @@ jest.mock('../../../api/components', () => ({
getComponentData: jest.fn().mockResolvedValue({ analysisDate: '2018-07-30' })
}));
+jest.mock('../../../api/measures', () => ({
+ getMeasures: jest
+ .fn()
+ .mockResolvedValue([
+ { metric: 'new_coverage', value: '0', periods: [{ index: 1, value: '95.9943' }] },
+ { metric: 'coverage', value: '99.3' }
+ ])
+}));
+
jest.mock('../../../api/nav', () => ({
getComponentNavigation: jest.fn().mockResolvedValue({
breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }],
@@ -68,6 +78,7 @@ beforeEach(() => {
(getComponentData as jest.Mock).mockClear();
(getComponentNavigation as jest.Mock).mockClear();
(getTasksForComponent as jest.Mock).mockClear();
+ (getMeasures as jest.Mock).mockClear();
});
it('changes component', () => {
@@ -144,6 +155,40 @@ it('updates branches on change', () => {
expect(getPullRequests).toBeCalledWith('projectKey');
});
+it('updates the branch measures', async () => {
+ (getComponentNavigation as jest.Mock<any>).mockResolvedValueOnce({
+ breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: 'TRK' }],
+ key: 'foo'
+ });
+ (getBranches as jest.Mock<any>).mockResolvedValueOnce([
+ { isMain: false, mergeBranch: 'master', name: 'feature', type: BranchType.SHORT }
+ ]);
+ (getPullRequests as jest.Mock<any>).mockResolvedValueOnce([]);
+ const wrapper = shallow(
+ <ComponentContainer
+ fetchOrganizations={jest.fn()}
+ location={{ query: { id: 'foo', branch: 'feature' } }}>
+ <Inner />
+ </ComponentContainer>
+ );
+ (wrapper.instance() as ComponentContainer).mounted = true;
+ wrapper.setState({
+ branches: [{ isMain: true }],
+ component: { breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: 'TRK' }] },
+ loading: false
+ });
+
+ await new Promise(setImmediate);
+ expect(getBranches).toBeCalledWith('foo');
+
+ await new Promise(setImmediate);
+ expect(getMeasures).toBeCalledWith({
+ componentKey: 'foo',
+ metricKeys: 'coverage,new_coverage',
+ branch: 'feature'
+ });
+});
+
it('loads organization', async () => {
(getComponentData as jest.Mock<any>).mockResolvedValueOnce({ organization: 'org' });
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
index 8f9d34a94fb..7024131f79b 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
@@ -24,7 +24,7 @@ import ComponentNavMenu from './ComponentNavMenu';
import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif';
import RecentHistory from '../../RecentHistory';
import * as theme from '../../../theme';
-import { BranchLike, Component } from '../../../types';
+import { BranchLike, Component, Measure } from '../../../types';
import ContextNavBar from '../../../../components/nav/ContextNavBar';
import { Task } from '../../../../api/ce';
import { STATUSES } from '../../../../apps/background-tasks/constants';
@@ -32,6 +32,7 @@ import './ComponentNav.css';
interface Props {
branchLikes: BranchLike[];
+ branchMeasures?: Measure[];
currentBranchLike: BranchLike | undefined;
component: Component;
currentTask?: Task;
@@ -92,7 +93,11 @@ export default class ComponentNav extends React.PureComponent<Props> {
// to close dropdown on any location change
location={this.props.location}
/>
- <ComponentNavMeta branchLike={currentBranchLike} component={component} />
+ <ComponentNavMeta
+ branchLike={currentBranchLike}
+ branchMeasures={this.props.branchMeasures}
+ component={component}
+ />
</div>
<ComponentNavMenu
branchLike={currentBranchLike}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
index 51babd7921b..a1e503abd3d 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
@@ -25,8 +25,10 @@ import {
CurrentUser,
isLoggedIn,
HomePageType,
- HomePage
+ HomePage,
+ Measure
} from '../../../types';
+import BranchMeasures from '../../../../components/common/BranchMeasures';
import BranchStatus from '../../../../components/common/BranchStatus';
import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
import Favorite from '../../../../components/controls/Favorite';
@@ -48,10 +50,11 @@ interface StateProps {
interface Props extends StateProps {
branchLike?: BranchLike;
+ branchMeasures?: Measure[];
component: Component;
}
-export function ComponentNavMeta({ branchLike, component, currentUser }: Props) {
+export function ComponentNavMeta({ branchLike, branchMeasures, component, currentUser }: Props) {
const mainBranch = !branchLike || isMainBranch(branchLike);
const longBranch = isLongLivingBranch(branchLike);
const currentPage = getCurrentPage(component, branchLike);
@@ -87,7 +90,7 @@ export function ComponentNavMeta({ branchLike, component, currentUser }: Props)
</div>
)}
{(isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && (
- <div className="navbar-context-meta-secondary">
+ <div className="navbar-context-meta-secondary display-inline-flex-center">
{isPullRequest(branchLike) &&
branchLike.url !== undefined && (
<a
@@ -100,6 +103,13 @@ export function ComponentNavMeta({ branchLike, component, currentUser }: Props)
</a>
)}
<BranchStatus branchLike={branchLike} />
+ {branchMeasures &&
+ branchMeasures.length > 0 && (
+ <>
+ <span className="vertical-separator" />
+ <BranchMeasures measures={branchMeasures} />
+ </>
+ )}
</div>
)}
</div>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
index c6bff99c4a9..74ad5181300 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
@@ -22,7 +22,7 @@ import { shallow } from 'enzyme';
import { ComponentNavMeta } from '../ComponentNavMeta';
import { BranchType, ShortLivingBranch, LongLivingBranch, PullRequest } from '../../../../types';
-const component = {
+const COMPONENT = {
analysisDate: '2017-01-02T00:00:00.000Z',
breadcrumbs: [],
key: 'foo',
@@ -32,6 +32,11 @@ const component = {
version: '0.0.1'
};
+const MEASURES = [
+ { metric: 'new_coverage', value: '0', periods: [{ index: 1, value: '95.9943' }] },
+ { metric: 'coverage', value: '99.3' }
+];
+
it('renders status of short-living branch', () => {
const branch: ShortLivingBranch = {
isMain: false,
@@ -44,7 +49,8 @@ it('renders status of short-living branch', () => {
shallow(
<ComponentNavMeta
branchLike={branch}
- component={component}
+ branchMeasures={MEASURES}
+ component={COMPONENT}
currentUser={{ isLoggedIn: false }}
/>
)
@@ -62,7 +68,7 @@ it('renders meta for long-living branch', () => {
shallow(
<ComponentNavMeta
branchLike={branch}
- component={component}
+ component={COMPONENT}
currentUser={{ isLoggedIn: false }}
/>
)
@@ -82,7 +88,7 @@ it('renders meta for pull request', () => {
shallow(
<ComponentNavMeta
branchLike={pullRequest}
- component={component}
+ component={COMPONENT}
currentUser={{ isLoggedIn: false }}
/>
)
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
index 144ce956ea4..f618947a93d 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
@@ -38,7 +38,7 @@ exports[`renders meta for pull request 1`] = `
/>
</div>
<div
- className="navbar-context-meta-secondary"
+ className="navbar-context-meta-secondary display-inline-flex-center"
>
<a
className="display-inline-flex-center big-spacer-right"
@@ -85,7 +85,7 @@ exports[`renders status of short-living branch 1`] = `
/>
</div>
<div
- className="navbar-context-meta-secondary"
+ className="navbar-context-meta-secondary display-inline-flex-center"
>
<BranchStatus
branchLike={
@@ -103,6 +103,31 @@ exports[`renders status of short-living branch 1`] = `
}
}
/>
+ <React.Fragment>
+ <span
+ className="vertical-separator"
+ />
+ <BranchMeasures
+ measures={
+ Array [
+ Object {
+ "metric": "new_coverage",
+ "periods": Array [
+ Object {
+ "index": 1,
+ "value": "95.9943",
+ },
+ ],
+ "value": "0",
+ },
+ Object {
+ "metric": "coverage",
+ "value": "99.3",
+ },
+ ]
+ }
+ />
+ </React.Fragment>
</div>
</div>
`;
diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css
index 9b7c566d681..dcd5aff9e14 100644
--- a/server/sonar-web/src/main/js/app/styles/init/misc.css
+++ b/server/sonar-web/src/main/js/app/styles/init/misc.css
@@ -348,6 +348,16 @@ td.big-spacer-top {
color: rgba(68, 68, 68, 0.3);
}
+.vertical-separator {
+ margin-left: calc(2 * var(--gridSize));
+ margin-right: calc(2 * var(--gridSize));
+}
+
+.vertical-separator:after {
+ content: '|';
+ color: var(--barBorderColor);
+}
+
.capitalize {
text-transform: capitalize !important;
}
@@ -389,3 +399,9 @@ td.big-spacer-top {
background-color: var(--barBackgroundColor);
color: inherit;
}
+
+.leak-box {
+ background-color: var(--leakColor);
+ border: 1px solid var(--leakBorderColor);
+ padding: 4px 6px;
+}
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index 954eecdd305..254817b7430 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -108,6 +108,7 @@ export interface ComponentQualityProfile {
interface ComponentMeasureIntern {
branch?: string;
+ description?: string;
isFavorite?: boolean;
isRecentlyBrowsed?: boolean;
key: string;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx
index f1d8747a2c8..57362e1933f 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx
@@ -46,7 +46,7 @@ export default class LeakPeriodLegend extends React.PureComponent<Props> {
render() {
const { className, component, period } = this.props;
- const leakClass = classNames('domain-measures-leak-header', className);
+ const leakClass = classNames('domain-measures-header leak-box', className);
if (component.qualifier === 'APP') {
return <div className={leakClass}>{translate('issues.new_code_period')}</div>;
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
index 2d524d52332..d85dcc4a067 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
@@ -56,7 +56,7 @@ export default function MeasureHeader(props: Props) {
<strong>
{isDiff ? (
<Measure
- className="domain-measures-leak"
+ className="leak-box"
metricKey={metric.key}
metricType={metric.type}
value={measure && measure.leak}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.tsx.snap
index 09d9f2e8e41..80065f588e8 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.tsx.snap
@@ -15,7 +15,7 @@ exports[`should render a more precise date 1`] = `
}
>
<div
- className="domain-measures-leak-header"
+ className="domain-measures-header leak-box"
>
overview.new_code_period_x.overview.period.previous_version.6,4
</div>
@@ -38,7 +38,7 @@ exports[`should render correctly 1`] = `
}
>
<div
- className="domain-measures-leak-header"
+ className="domain-measures-header leak-box"
>
overview.new_code_period_x.overview.period.previous_version.6,4
</div>
@@ -47,7 +47,7 @@ exports[`should render correctly 1`] = `
exports[`should render correctly 2`] = `
<div
- className="domain-measures-leak-header"
+ className="domain-measures-header leak-box"
>
overview.new_code_period_x.overview.period.days.18
</div>
@@ -55,7 +55,7 @@ exports[`should render correctly 2`] = `
exports[`should render correctly for APP 1`] = `
<div
- className="domain-measures-leak-header"
+ className="domain-measures-header leak-box"
>
issues.new_code_period
</div>
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap
index c74f60aa659..7a4677c1b7c 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap
@@ -94,7 +94,7 @@ exports[`should render correctly for leak 1`] = `
>
<strong>
<Measure
- className="domain-measures-leak"
+ className="leak-box"
metricKey="new_reliability_rating"
metricType="RATING"
value="3.0"
@@ -169,7 +169,7 @@ exports[`should render with short living branch 1`] = `
>
<strong>
<Measure
- className="domain-measures-leak"
+ className="leak-box"
metricKey="new_reliability_rating"
metricType="RATING"
value="3.0"
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx
index 1829547f860..473b287cbd7 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx
@@ -29,9 +29,7 @@ interface Props {
export default function FacetMeasureValue({ measure }: Props) {
if (isDiffMetric(measure.metric.key)) {
return (
- <div
- className="domain-measures-value domain-measures-leak"
- id={`measure-${measure.metric.key}-leak`}>
+ <div className="domain-measures-value leak-box" id={`measure-${measure.metric.key}-leak`}>
<Measure
metricKey={measure.metric.key}
metricType={measure.metric.type}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap
index 228f8a3e6d9..94bc6ea97f5 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap
@@ -2,7 +2,7 @@
exports[`should display leak measure value 1`] = `
<div
- className="domain-measures-value domain-measures-leak"
+ className="domain-measures-value leak-box"
id="measure-new_bugs-leak"
>
<Measure
diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css
index a5e6b8dbac3..e47d25cb92d 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/style.css
+++ b/server/sonar-web/src/main/js/apps/component-measures/style.css
@@ -30,13 +30,7 @@
margin-right: -4px;
}
-.domain-measures-leak {
- background-color: var(--leakColor);
- border: 1px solid var(--leakBorderColor);
- padding: 4px 6px;
-}
-
-.search-navigator-facet .domain-measures-leak {
+.search-navigator-facet .leak-box {
height: var(--controlHeight);
line-height: var(--controlHeight);
padding: 0 var(--gridSize);
@@ -46,8 +40,8 @@
box-sizing: border-box;
}
-.search-navigator-facet:hover .domain-measures-leak,
-.search-navigator-facet.active .domain-measures-leak {
+.search-navigator-facet:hover .leak-box,
+.search-navigator-facet.active .leak-box {
height: calc(var(--controlHeight) - 2px);
margin-top: 0;
margin-right: calc(-0.75 * var(--gridSize));
@@ -58,16 +52,14 @@
border-bottom-left-radius: 0;
}
-.search-navigator-facet.active .domain-measures-leak {
+.search-navigator-facet.active .leak-box {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
-.domain-measures-leak-header {
+.domain-measures-header {
display: inline-block;
- background-color: var(--leakColor);
- border: 1px solid var(--leakBorderColor);
padding: 4px 10px;
white-space: nowrap;
}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
index f6e58af8c0b..f47adbefeba 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
@@ -107,12 +107,12 @@ export class OverviewApp extends React.PureComponent<Props, State> {
additionalFields: 'metrics,periods',
...getBranchLikeQuery(branchLike)
}).then(
- r => {
- if (this.mounted && r.metrics) {
+ ({ component, metrics, periods }) => {
+ if (this.mounted && metrics && component.measures) {
this.setState({
loading: false,
- measures: enhanceMeasuresWithMetrics(r.component.measures, r.metrics),
- periods: r.periods
+ measures: enhanceMeasuresWithMetrics(component.measures, metrics),
+ periods
});
}
},
diff --git a/server/sonar-web/src/main/js/components/common/BranchMeasures.tsx b/server/sonar-web/src/main/js/components/common/BranchMeasures.tsx
new file mode 100644
index 00000000000..eed1bf35e28
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/BranchMeasures.tsx
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import * as classNames from 'classnames';
+import { Measure } from '../../app/types';
+import { getLeakValue } from '../measure/utils';
+import CoverageRating from '../ui/CoverageRating';
+import { formatMeasure, isDiffMetric } from '../../helpers/measures';
+import HelpTooltip from '../controls/HelpTooltip';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+ measures: Measure[];
+}
+
+export default function BranchMeasures({ measures }: Props) {
+ const coverage = measures.find(measure => measure.metric === 'coverage');
+ const newCoverage = measures.find(measure => measure.metric === 'new_coverage');
+ if (!coverage && !newCoverage) {
+ return null;
+ }
+
+ return (
+ <div className="display-inline-flex-center">
+ {coverage && <BranchCoverage measure={coverage} />}
+ {newCoverage && (
+ <BranchCoverage
+ className={classNames({ 'big-spacer-left': Boolean(coverage) })}
+ measure={newCoverage}
+ />
+ )}
+ </div>
+ );
+}
+
+interface MeasureProps {
+ className?: string;
+ measure: Measure;
+}
+
+export function BranchCoverage({ className, measure }: MeasureProps) {
+ const isDiff = isDiffMetric(measure.metric);
+ const value = isDiff ? getLeakValue(measure) : measure.value;
+ return (
+ <div
+ className={classNames(
+ 'display-inline-flex-center',
+ { 'rounded leak-box': isDiff },
+ className
+ )}>
+ <CoverageRating size="xs" value={value} />
+ <span className="little-spacer-left">{formatMeasure(value, 'PERCENT')}</span>
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay={translate('branches.measures', measure.metric, 'help')}
+ />
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BranchMeasures-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/BranchMeasures-test.tsx
new file mode 100644
index 00000000000..0f50823cc7a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/BranchMeasures-test.tsx
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import BranchMeasures, { BranchCoverage } from '../BranchMeasures';
+
+const MEASURES = [
+ { metric: 'new_coverage', value: '0', periods: [{ index: 1, value: '95.9943' }] },
+ { metric: 'coverage', value: '99.3' }
+];
+
+describe('BranchMeasures', () => {
+ it('should render coverage measures', () => {
+ expect(shallow(<BranchMeasures measures={MEASURES} />)).toMatchSnapshot();
+ });
+
+ it('should render correctly when a coverage measure is missing', () => {
+ expect(shallow(<BranchMeasures measures={[MEASURES[0]]} />)).toMatchSnapshot();
+ });
+
+ it('should not render anything', () => {
+ expect(shallow(<BranchMeasures measures={[]} />).type()).toBeNull();
+ });
+});
+
+describe('BranchCoverage', () => {
+ it('should render correctly', () => {
+ expect(shallow(<BranchCoverage measure={MEASURES[1]} />)).toMatchSnapshot();
+ });
+
+ it('should render leak measure correctly', () => {
+ expect(shallow(<BranchCoverage measure={MEASURES[0]} />)).toMatchSnapshot();
+ });
+});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchMeasures-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchMeasures-test.tsx.snap
new file mode 100644
index 00000000000..153853dfec9
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchMeasures-test.tsx.snap
@@ -0,0 +1,93 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BranchCoverage should render correctly 1`] = `
+<div
+ className="display-inline-flex-center"
+>
+ <CoverageRating
+ size="xs"
+ value="99.3"
+ />
+ <span
+ className="little-spacer-left"
+ >
+ 99.3%
+ </span>
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay="branches.measures.coverage.help"
+ />
+</div>
+`;
+
+exports[`BranchCoverage should render leak measure correctly 1`] = `
+<div
+ className="display-inline-flex-center rounded leak-box"
+>
+ <CoverageRating
+ size="xs"
+ value="95.9943"
+ />
+ <span
+ className="little-spacer-left"
+ >
+ 96.0%
+ </span>
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay="branches.measures.new_coverage.help"
+ />
+</div>
+`;
+
+exports[`BranchMeasures should render correctly when a coverage measure is missing 1`] = `
+<div
+ className="display-inline-flex-center"
+>
+ <BranchCoverage
+ className=""
+ measure={
+ Object {
+ "metric": "new_coverage",
+ "periods": Array [
+ Object {
+ "index": 1,
+ "value": "95.9943",
+ },
+ ],
+ "value": "0",
+ }
+ }
+ />
+</div>
+`;
+
+exports[`BranchMeasures should render coverage measures 1`] = `
+<div
+ className="display-inline-flex-center"
+>
+ <BranchCoverage
+ measure={
+ Object {
+ "metric": "coverage",
+ "value": "99.3",
+ }
+ }
+ />
+ <BranchCoverage
+ className="big-spacer-left"
+ measure={
+ Object {
+ "metric": "new_coverage",
+ "periods": Array [
+ Object {
+ "index": 1,
+ "value": "95.9943",
+ },
+ ],
+ "value": "0",
+ }
+ }
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
index f4116968f6b..3dd7bd6ef40 100644
--- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
+++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
@@ -84,7 +84,7 @@
.navbar-context-meta-secondary {
position: absolute;
- top: 36px;
+ top: 34px;
right: 0;
padding: 0 20px;
white-space: nowrap;
diff --git a/server/sonar-web/src/main/js/components/ui/CoverageRating.tsx b/server/sonar-web/src/main/js/components/ui/CoverageRating.tsx
index 6814eea2f1d..f6cafaa72de 100644
--- a/server/sonar-web/src/main/js/components/ui/CoverageRating.tsx
+++ b/server/sonar-web/src/main/js/components/ui/CoverageRating.tsx
@@ -23,13 +23,13 @@ import * as theme from '../../app/theme';
const DonutChart = lazyLoad(() => import('../charts/DonutChart'));
-const SIZE_TO_WIDTH_MAPPING = { small: 16, normal: 24, big: 40, huge: 60 };
+const SIZE_TO_WIDTH_MAPPING = { xs: 12, small: 16, normal: 24, big: 40, huge: 60 };
-const SIZE_TO_THICKNESS_MAPPING = { small: 2, normal: 3, big: 3, huge: 4 };
+const SIZE_TO_THICKNESS_MAPPING = { xs: 2, small: 2, normal: 3, big: 3, huge: 4 };
interface Props {
muted?: boolean;
- size?: 'small' | 'normal' | 'big' | 'huge';
+ size?: 'xs' | 'small' | 'normal' | 'big' | 'huge';
value: number | string | null | undefined;
}