@@ -38,6 +38,7 @@ import { IssueType, MeasurementType } from '../utils'; | |||
import DebtValue from './DebtValue'; | |||
import { DrilldownMeasureValue } from './DrilldownMeasureValue'; | |||
import { LeakPeriodInfo } from './LeakPeriodInfo'; | |||
import SecurityHotspotsReviewed from './SecurityHotspotsReviewed'; | |||
export interface MeasuresPanelProps { | |||
branchLike?: BranchLike; | |||
@@ -60,9 +61,11 @@ export function MeasuresPanel(props: MeasuresPanelProps) { | |||
const [tab, selectTab] = React.useState(MeasuresPanelTabs.New); | |||
const isNewCodeTab = tab === MeasuresPanelTabs.New; | |||
React.useEffect(() => { | |||
// Open Overall tab by default if there are no new measures. | |||
if (loading === false && !hasDiffMeasures && tab === MeasuresPanelTabs.New) { | |||
if (loading === false && !hasDiffMeasures && isNewCodeTab) { | |||
selectTab(MeasuresPanelTabs.Overall); | |||
} | |||
// In this case, we explicitly do NOT want to mark tab as a dependency, as | |||
@@ -105,7 +108,7 @@ export function MeasuresPanel(props: MeasuresPanelProps) { | |||
<BoxedTabs onSelect={selectTab} selected={tab} tabs={tabs} /> | |||
<div className="overview-panel-content flex-1 bordered"> | |||
{!hasDiffMeasures && tab === MeasuresPanelTabs.New ? ( | |||
{!hasDiffMeasures && isNewCodeTab ? ( | |||
<div | |||
className="display-flex-center display-flex-justify-center" | |||
style={{ height: 500 }}> | |||
@@ -136,73 +139,75 @@ export function MeasuresPanel(props: MeasuresPanelProps) { | |||
</div> | |||
) : ( | |||
<> | |||
{[IssueType.Bug, IssueType.Vulnerability, IssueType.CodeSmell].map( | |||
(type: IssueType) => ( | |||
<div | |||
className="display-flex-row overview-measures-row" | |||
data-test={`overview__measures-${type.toString().toLowerCase()}`} | |||
key={type}> | |||
{type === IssueType.CodeSmell ? ( | |||
<> | |||
<div className="overview-panel-big-padded flex-1 small display-flex-center big-spacer-left"> | |||
<DebtValue | |||
branchLike={branchLike} | |||
component={component} | |||
measures={measures} | |||
useDiffMetric={tab === MeasuresPanelTabs.New} | |||
/> | |||
</div> | |||
<div className="flex-1 small display-flex-center"> | |||
<IssueLabel | |||
branchLike={branchLike} | |||
component={component} | |||
measures={measures} | |||
type={type} | |||
useDiffMetric={tab === MeasuresPanelTabs.New} | |||
/> | |||
</div> | |||
</> | |||
) : ( | |||
<> | |||
<div className="overview-panel-big-padded flex-1 small display-flex-center big-spacer-left"> | |||
<IssueLabel | |||
branchLike={branchLike} | |||
component={component} | |||
measures={measures} | |||
type={type} | |||
useDiffMetric={tab === MeasuresPanelTabs.New} | |||
/> | |||
</div> | |||
{type === 'VULNERABILITY' && ( | |||
<div className="flex-1 small display-flex-center"> | |||
<IssueLabel | |||
branchLike={branchLike} | |||
component={component} | |||
docTooltip={import( | |||
/* webpackMode: "eager" */ 'Docs/tooltips/metrics/security-hotspots.md' | |||
)} | |||
measures={measures} | |||
type={IssueType.SecurityHotspot} | |||
useDiffMetric={tab === MeasuresPanelTabs.New} | |||
/> | |||
</div> | |||
)} | |||
</> | |||
)} | |||
{(!isApp || tab === MeasuresPanelTabs.Overall) && ( | |||
<div className="overview-panel-big-padded overview-measures-aside display-flex-center"> | |||
<IssueRating | |||
{[ | |||
IssueType.Bug, | |||
IssueType.Vulnerability, | |||
IssueType.SecurityHotspot, | |||
IssueType.CodeSmell | |||
].map((type: IssueType) => ( | |||
<div | |||
className="display-flex-row overview-measures-row" | |||
data-test={`overview__measures-${type.toString().toLowerCase()}`} | |||
key={type}> | |||
{type === IssueType.CodeSmell ? ( | |||
<> | |||
<div className="overview-panel-big-padded flex-1 small display-flex-center big-spacer-left"> | |||
<DebtValue | |||
branchLike={branchLike} | |||
component={component} | |||
measures={measures} | |||
useDiffMetric={isNewCodeTab} | |||
/> | |||
</div> | |||
<div className="flex-1 small display-flex-center"> | |||
<IssueLabel | |||
branchLike={branchLike} | |||
component={component} | |||
measures={measures} | |||
type={type} | |||
useDiffMetric={tab === MeasuresPanelTabs.New} | |||
useDiffMetric={isNewCodeTab} | |||
/> | |||
</div> | |||
)} | |||
</div> | |||
) | |||
)} | |||
</> | |||
) : ( | |||
<div className="overview-panel-big-padded flex-1 small display-flex-center big-spacer-left"> | |||
<IssueLabel | |||
branchLike={branchLike} | |||
component={component} | |||
docTooltip={ | |||
type === IssueType.SecurityHotspot | |||
? import( | |||
/* webpackMode: "eager" */ 'Docs/tooltips/metrics/security-hotspots.md' | |||
) | |||
: undefined | |||
} | |||
measures={measures} | |||
type={type} | |||
useDiffMetric={isNewCodeTab} | |||
/> | |||
</div> | |||
)} | |||
{type === IssueType.SecurityHotspot && ( | |||
<div className="flex-1 small display-flex-center"> | |||
<SecurityHotspotsReviewed | |||
measures={measures} | |||
useDiffMetric={isNewCodeTab} | |||
/> | |||
</div> | |||
)} | |||
{(!isApp || tab === MeasuresPanelTabs.Overall) && ( | |||
<div className="overview-panel-big-padded overview-measures-aside display-flex-center"> | |||
<IssueRating | |||
branchLike={branchLike} | |||
component={component} | |||
measures={measures} | |||
type={type} | |||
useDiffMetric={isNewCodeTab} | |||
/> | |||
</div> | |||
)} | |||
</div> | |||
))} | |||
<div className="display-flex-row overview-measures-row"> | |||
{(findMeasure(measures, MetricKey.coverage) || | |||
@@ -212,11 +217,11 @@ export function MeasuresPanel(props: MeasuresPanelProps) { | |||
data-test="overview__measures-coverage"> | |||
<MeasurementLabel | |||
branchLike={branchLike} | |||
centered={tab === MeasuresPanelTabs.New} | |||
centered={isNewCodeTab} | |||
component={component} | |||
measures={measures} | |||
type={MeasurementType.Coverage} | |||
useDiffMetric={tab === MeasuresPanelTabs.New} | |||
useDiffMetric={isNewCodeTab} | |||
/> | |||
{tab === MeasuresPanelTabs.Overall && ( | |||
@@ -234,11 +239,11 @@ export function MeasuresPanel(props: MeasuresPanelProps) { | |||
<div className="overview-panel-huge-padded flex-1 display-flex-center"> | |||
<MeasurementLabel | |||
branchLike={branchLike} | |||
centered={tab === MeasuresPanelTabs.New} | |||
centered={isNewCodeTab} | |||
component={component} | |||
measures={measures} | |||
type={MeasurementType.Duplication} | |||
useDiffMetric={tab === MeasuresPanelTabs.New} | |||
useDiffMetric={isNewCodeTab} | |||
/> | |||
{tab === MeasuresPanelTabs.Overall && ( |
@@ -0,0 +1,56 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { formatMeasure } from 'sonar-ui-common/helpers/measures'; | |||
import { getLeakValue } from '../../../components/measure/utils'; | |||
import { findMeasure } from '../../../helpers/measures'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
export interface SecurityHotspotsReviewedProps { | |||
measures: T.MeasureEnhanced[]; | |||
useDiffMetric?: boolean; | |||
} | |||
export default function SecurityHotspotsReviewed(props: SecurityHotspotsReviewedProps) { | |||
const { measures, useDiffMetric = false } = props; | |||
const metric = useDiffMetric | |||
? MetricKey.new_security_hotspots_reviewed | |||
: MetricKey.security_hotspots_reviewed; | |||
const measure = findMeasure(measures, metric); | |||
let value; | |||
if (measure) { | |||
value = useDiffMetric ? getLeakValue(measure) : measure.value; | |||
} | |||
return ( | |||
<> | |||
{value === undefined ? ( | |||
<span aria-label={translate('no_data')} className="overview-measures-empty-value" /> | |||
) : ( | |||
<span className="huge">{formatMeasure(value, 'PERCENT')}</span> | |||
)} | |||
<span className="big-spacer-left"> | |||
{translate('overview.measures.security_hotspots_reviewed')} | |||
</span> | |||
</> | |||
); | |||
} |
@@ -0,0 +1,46 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import SecurityHotspotsReviewed, { | |||
SecurityHotspotsReviewedProps | |||
} from '../SecurityHotspotsReviewed'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ useDiffMetric: true })).toMatchSnapshot('on new code'); | |||
expect(shallowRender({ measures: [] })).toMatchSnapshot('no measures'); | |||
}); | |||
function shallowRender(props: Partial<SecurityHotspotsReviewedProps> = {}) { | |||
return shallow( | |||
<SecurityHotspotsReviewed | |||
measures={[ | |||
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.security_hotspots_reviewed }) }), | |||
mockMeasureEnhanced({ | |||
metric: mockMetric({ key: MetricKey.new_security_hotspots_reviewed }) | |||
}) | |||
]} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -268,6 +268,64 @@ exports[`application overview should render correctly 1`] = ` | |||
], | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"metric": Object { | |||
"id": "security_hotspots_reviewed", | |||
"key": "security_hotspots_reviewed", | |||
"name": "security_hotspots_reviewed", | |||
"type": "INT", | |||
}, | |||
"periods": undefined, | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "new_security_hotspots_reviewed", | |||
"key": "new_security_hotspots_reviewed", | |||
"name": "new_security_hotspots_reviewed", | |||
"type": "INT", | |||
}, | |||
"periods": Array [ | |||
Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
], | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"metric": Object { | |||
"id": "security_review_rating", | |||
"key": "security_review_rating", | |||
"name": "security_review_rating", | |||
"type": "RATING", | |||
}, | |||
"periods": undefined, | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "new_security_review_rating", | |||
"key": "new_security_review_rating", | |||
"name": "new_security_review_rating", | |||
"type": "RATING", | |||
}, | |||
"periods": Array [ | |||
Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
], | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"metric": Object { | |||
@@ -660,6 +718,30 @@ exports[`application overview should render correctly 1`] = ` | |||
"name": "new_security_hotspots", | |||
"type": "INT", | |||
}, | |||
Object { | |||
"id": "security_hotspots_reviewed", | |||
"key": "security_hotspots_reviewed", | |||
"name": "security_hotspots_reviewed", | |||
"type": "INT", | |||
}, | |||
Object { | |||
"id": "new_security_hotspots_reviewed", | |||
"key": "new_security_hotspots_reviewed", | |||
"name": "new_security_hotspots_reviewed", | |||
"type": "INT", | |||
}, | |||
Object { | |||
"id": "security_review_rating", | |||
"key": "security_review_rating", | |||
"name": "security_review_rating", | |||
"type": "RATING", | |||
}, | |||
Object { | |||
"id": "new_security_review_rating", | |||
"key": "new_security_review_rating", | |||
"name": "new_security_review_rating", | |||
"type": "RATING", | |||
}, | |||
Object { | |||
"id": "code_smells", | |||
"key": "code_smells", | |||
@@ -1131,6 +1213,64 @@ exports[`project overview should render correctly 1`] = ` | |||
], | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"metric": Object { | |||
"id": "security_hotspots_reviewed", | |||
"key": "security_hotspots_reviewed", | |||
"name": "security_hotspots_reviewed", | |||
"type": "INT", | |||
}, | |||
"periods": undefined, | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "new_security_hotspots_reviewed", | |||
"key": "new_security_hotspots_reviewed", | |||
"name": "new_security_hotspots_reviewed", | |||
"type": "INT", | |||
}, | |||
"periods": Array [ | |||
Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
], | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"metric": Object { | |||
"id": "security_review_rating", | |||
"key": "security_review_rating", | |||
"name": "security_review_rating", | |||
"type": "RATING", | |||
}, | |||
"periods": undefined, | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "new_security_review_rating", | |||
"key": "new_security_review_rating", | |||
"name": "new_security_review_rating", | |||
"type": "RATING", | |||
}, | |||
"periods": Array [ | |||
Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
], | |||
"value": "1.0", | |||
}, | |||
Object { | |||
"bestValue": true, | |||
"metric": Object { | |||
@@ -1523,6 +1663,30 @@ exports[`project overview should render correctly 1`] = ` | |||
"name": "new_security_hotspots", | |||
"type": "INT", | |||
}, | |||
Object { | |||
"id": "security_hotspots_reviewed", | |||
"key": "security_hotspots_reviewed", | |||
"name": "security_hotspots_reviewed", | |||
"type": "INT", | |||
}, | |||
Object { | |||
"id": "new_security_hotspots_reviewed", | |||
"key": "new_security_hotspots_reviewed", | |||
"name": "new_security_hotspots_reviewed", | |||
"type": "INT", | |||
}, | |||
Object { | |||
"id": "security_review_rating", | |||
"key": "security_review_rating", | |||
"name": "security_review_rating", | |||
"type": "RATING", | |||
}, | |||
Object { | |||
"id": "new_security_review_rating", | |||
"key": "new_security_review_rating", | |||
"name": "new_security_review_rating", | |||
"type": "RATING", | |||
}, | |||
Object { | |||
"id": "code_smells", | |||
"key": "code_smells", |
@@ -0,0 +1,45 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<span | |||
className="huge" | |||
> | |||
1.0% | |||
</span> | |||
<span | |||
className="big-spacer-left" | |||
> | |||
overview.measures.security_hotspots_reviewed | |||
</span> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: no measures 1`] = ` | |||
<Fragment> | |||
<span | |||
aria-label="no_data" | |||
className="overview-measures-empty-value" | |||
/> | |||
<span | |||
className="big-spacer-left" | |||
> | |||
overview.measures.security_hotspots_reviewed | |||
</span> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: on new code 1`] = ` | |||
<Fragment> | |||
<span | |||
className="huge" | |||
> | |||
1.0% | |||
</span> | |||
<span | |||
className="big-spacer-left" | |||
> | |||
overview.measures.security_hotspots_reviewed | |||
</span> | |||
</Fragment> | |||
`; |
@@ -65,7 +65,7 @@ export function IssueLabel(props: IssueLabelProps) { | |||
className="overview-measures-value text-light" | |||
to={ | |||
type === IssueType.SecurityHotspot | |||
? getComponentSecurityHotspotsUrl(component.key, getBranchLikeQuery(branchLike)) | |||
? getComponentSecurityHotspotsUrl(component.key, params) | |||
: getComponentIssuesUrl(component.key, params) | |||
}> | |||
{formatMeasure(value, 'SHORT_INT')} |
@@ -122,8 +122,12 @@ exports[`should render correctly for hotspots 1`] = ` | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"assignedToMe": undefined, | |||
"branch": undefined, | |||
"hotspots": undefined, | |||
"id": "my-project", | |||
"pullRequest": "1001", | |||
"sinceLeakPeriod": "false", | |||
}, | |||
} | |||
} | |||
@@ -151,8 +155,12 @@ exports[`should render correctly for hotspots 2`] = ` | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"assignedToMe": undefined, | |||
"branch": undefined, | |||
"hotspots": undefined, | |||
"id": "my-project", | |||
"pullRequest": "1001", | |||
"sinceLeakPeriod": "true", | |||
}, | |||
} | |||
} |
@@ -75,7 +75,7 @@ | |||
} | |||
.overview-measures-aside { | |||
flex-basis: 180px; | |||
flex-basis: 200px; | |||
box-sizing: border-box; | |||
} | |||
@@ -43,8 +43,14 @@ export const METRICS: string[] = [ | |||
MetricKey.new_vulnerabilities, | |||
MetricKey.security_rating, | |||
MetricKey.new_security_rating, | |||
// hotspots | |||
MetricKey.security_hotspots, | |||
MetricKey.new_security_hotspots, | |||
MetricKey.security_hotspots_reviewed, | |||
MetricKey.new_security_hotspots_reviewed, | |||
MetricKey.security_review_rating, | |||
MetricKey.new_security_review_rating, | |||
// code smells | |||
MetricKey.code_smells, | |||
@@ -169,7 +175,6 @@ const ISSUETYPE_MAP = { | |||
rating: MetricKey.security_review_rating, | |||
newRating: MetricKey.new_security_review_rating, | |||
ratingName: 'SecurityReview', | |||
iconClass: SecurityHotspotIcon | |||
} | |||
}; |
@@ -20,6 +20,7 @@ | |||
import { | |||
getComponentDrilldownUrl, | |||
getComponentIssuesUrl, | |||
getComponentSecurityHotspotsUrl, | |||
getQualityGatesUrl, | |||
getQualityGateUrl | |||
} from '../urls'; | |||
@@ -45,6 +46,27 @@ describe('#getComponentIssuesUrl', () => { | |||
}); | |||
}); | |||
describe('getComponentSecurityHotspotsUrl', () => { | |||
it('should work with no extra parameters', () => { | |||
expect(getComponentSecurityHotspotsUrl(SIMPLE_COMPONENT_KEY, {})).toEqual({ | |||
pathname: '/security_hotspots', | |||
query: { id: SIMPLE_COMPONENT_KEY } | |||
}); | |||
}); | |||
it('should forward some query parameters', () => { | |||
expect( | |||
getComponentSecurityHotspotsUrl(SIMPLE_COMPONENT_KEY, { | |||
sinceLeakPeriod: 'true', | |||
ignoredParam: '1234' | |||
}) | |||
).toEqual({ | |||
pathname: '/security_hotspots', | |||
query: { id: SIMPLE_COMPONENT_KEY, sinceLeakPeriod: 'true' } | |||
}); | |||
}); | |||
}); | |||
describe('#getComponentDrilldownUrl', () => { | |||
it('should return component drilldown url', () => { | |||
expect( |
@@ -78,7 +78,11 @@ export function getComponentIssuesUrl(componentKey: string, query?: Query): Loca | |||
* Generate URL for a component's security hotspot page | |||
*/ | |||
export function getComponentSecurityHotspotsUrl(componentKey: string, query: Query = {}): Location { | |||
return { pathname: '/security_hotspots', query: { ...query, id: componentKey } }; | |||
const { branch, pullRequest, sinceLeakPeriod, hotspots, assignedToMe } = query; | |||
return { | |||
pathname: '/security_hotspots', | |||
query: { id: componentKey, branch, pullRequest, sinceLeakPeriod, hotspots, assignedToMe } | |||
}; | |||
} | |||
/** |
@@ -2742,6 +2742,7 @@ overview.recent_activity=Recent Activity | |||
overview.measures=Measures | |||
overview.measures.empty_explanation=Measures on New Code will appear after the second analysis of this branch. | |||
overview.measures.empty_link={learn_more_link} about the Clean as You Code approach. | |||
overview.measures.security_hotspots_reviewed=Reviewed | |||
overview.project.no_lines_of_code=This project has no lines of code. | |||
overview.project.empty=This project is empty. | |||
@@ -2750,6 +2751,7 @@ overview.project.branch_X_empty=The "{0}" branch of this project is empty. | |||
overview.project.main_branch_no_lines_of_code=The main branch has no lines of code. | |||
overview.project.main_branch_empty=The main branch of this project is empty. | |||
overview.project.branch_needs_new_analysis=The branch data is incomplete. Run a new analysis to update it. | |||
overview.coverage_on=Coverage on | |||
overview.coverage_on_X_lines=Coverage on {count} Lines to cover | |||
overview.coverage_on_X_new_lines=Coverage on {count} New Lines to cover |