diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-08-23 12:54:35 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2018-09-19 10:51:41 +0200 |
commit | 26e6aab3da5364bcb31fa8b23766610464a8781c (patch) | |
tree | 9902b310d48b56faed784e8a9a1a17e8ad2af782 /server | |
parent | bc4d2e6948669842ff325c789f570a4f46f0af41 (diff) | |
download | sonarqube-26e6aab3da5364bcb31fa8b23766610464a8781c.tar.gz sonarqube-26e6aab3da5364bcb31fa8b23766610464a8781c.zip |
SONAR-11159 SONAR-11160 Add new coverage and overall coverage next to branch status
Diffstat (limited to 'server')
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; } |