@@ -33,6 +33,7 @@ import NewSecurityFilter from '../filters/NewSecurityFilter'; | |||
import QualityGateFilter from '../filters/QualityGateFilter'; | |||
import ReliabilityFilter from '../filters/ReliabilityFilter'; | |||
import SecurityFilter from '../filters/SecurityFilter'; | |||
import SecurityReviewFilter from '../filters/SecurityReviewFilter'; | |||
import SizeFilter from '../filters/SizeFilter'; | |||
import TagsFilter from '../filters/TagsFilter'; | |||
import { hasFilterParams } from '../query'; | |||
@@ -91,6 +92,13 @@ export default function PageSidebar(props: Props) { | |||
facet={getFacet(facets, 'security')} | |||
value={query.security} | |||
/> | |||
<SecurityReviewFilter | |||
{...facetProps} | |||
facet={getFacet(facets, 'security_review_rating')} | |||
value={query.security_review_rating} | |||
/> | |||
<MaintainabilityFilter | |||
{...facetProps} | |||
facet={getFacet(facets, 'maintainability')} | |||
@@ -121,6 +129,13 @@ export default function PageSidebar(props: Props) { | |||
facet={getFacet(facets, 'new_security')} | |||
value={query.new_security} | |||
/> | |||
<SecurityReviewFilter | |||
{...facetProps} | |||
className="leak-facet-box" | |||
facet={getFacet(facets, 'new_security_review_rating')} | |||
property="new_security_review_rating" | |||
value={query.new_security_review_rating} | |||
/> | |||
<NewMaintainabilityFilter | |||
{...facetProps} | |||
facet={getFacet(facets, 'new_maintainability')} |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import BugIcon from 'sonar-ui-common/components/icons/BugIcon'; | |||
import CodeSmellIcon from 'sonar-ui-common/components/icons/CodeSmellIcon'; | |||
import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon'; | |||
import VulnerabilityIcon from 'sonar-ui-common/components/icons/VulnerabilityIcon'; | |||
import Rating from 'sonar-ui-common/components/ui/Rating'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
@@ -68,6 +69,24 @@ export default function ProjectCardLeakMeasures({ measures }: Props) { | |||
</div> | |||
</div> | |||
<div className="project-card-measure" data-key="new_security_review_rating"> | |||
<div className="project-card-measure-inner"> | |||
<div className="project-card-measure-number"> | |||
<Measure | |||
className="spacer-right" | |||
metricKey="new_security_hotspots_reviewed" | |||
metricType="PERCENT" | |||
value={measures['new_security_hotspots_reviewed']} | |||
/> | |||
<Rating value={measures['new_security_review_rating']} /> | |||
</div> | |||
<div className="project-card-measure-label-with-icon"> | |||
<SecurityHotspotIcon className="little-spacer-right text-bottom" /> | |||
{translate('metric.security_hotspots_reviewed.extra_short_name')} | |||
</div> | |||
</div> | |||
</div> | |||
<div className="project-card-measure" data-key="new_maintainability_rating"> | |||
<div className="project-card-measure-inner"> | |||
<div className="project-card-measure-number"> |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import BugIcon from 'sonar-ui-common/components/icons/BugIcon'; | |||
import CodeSmellIcon from 'sonar-ui-common/components/icons/CodeSmellIcon'; | |||
import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon'; | |||
import VulnerabilityIcon from 'sonar-ui-common/components/icons/VulnerabilityIcon'; | |||
import DuplicationsRating from 'sonar-ui-common/components/ui/DuplicationsRating'; | |||
import Rating from 'sonar-ui-common/components/ui/Rating'; | |||
@@ -81,6 +82,24 @@ export default function ProjectCardOverallMeasures({ measures }: Props) { | |||
</div> | |||
</div> | |||
<div className="project-card-measure" data-key="security_review_rating"> | |||
<div className="project-card-measure-inner"> | |||
<div className="project-card-measure-number"> | |||
<Measure | |||
className="spacer-right" | |||
metricKey="security_hotspots_reviewed" | |||
metricType="PERCENT" | |||
value={measures['security_hotspots_reviewed']} | |||
/> | |||
<Rating value={measures['security_review_rating']} /> | |||
</div> | |||
<div className="project-card-measure-label-with-icon"> | |||
<SecurityHotspotIcon className="little-spacer-right text-bottom" /> | |||
{translate('metric.security_hotspots_reviewed.extra_short_name')} | |||
</div> | |||
</div> | |||
</div> | |||
<div className="project-card-measure" data-key="sqale_rating"> | |||
<div className="project-card-measure-inner"> | |||
<div className="project-card-measure-number"> |
@@ -93,9 +93,11 @@ exports[`renders 1`] = ` | |||
"new_maintainability": undefined, | |||
"new_reliability": undefined, | |||
"new_security": undefined, | |||
"new_security_review_rating": undefined, | |||
"reliability": undefined, | |||
"search": undefined, | |||
"security": undefined, | |||
"security_review_rating": undefined, | |||
"size": undefined, | |||
"sort": undefined, | |||
"tags": undefined, | |||
@@ -148,9 +150,11 @@ exports[`renders 1`] = ` | |||
"new_maintainability": undefined, | |||
"new_reliability": undefined, | |||
"new_security": undefined, | |||
"new_security_review_rating": undefined, | |||
"reliability": undefined, | |||
"search": undefined, | |||
"security": undefined, | |||
"security_review_rating": undefined, | |||
"size": undefined, | |||
"sort": undefined, | |||
"tags": undefined, | |||
@@ -384,9 +388,11 @@ exports[`renders correctly empty organization 3`] = ` | |||
"new_maintainability": undefined, | |||
"new_reliability": undefined, | |||
"new_security": undefined, | |||
"new_security_review_rating": undefined, | |||
"reliability": undefined, | |||
"search": undefined, | |||
"security": undefined, | |||
"security_review_rating": undefined, | |||
"size": undefined, | |||
"sort": undefined, | |||
"tags": undefined, | |||
@@ -443,9 +449,11 @@ exports[`renders correctly empty organization 3`] = ` | |||
"new_maintainability": undefined, | |||
"new_reliability": undefined, | |||
"new_security": undefined, | |||
"new_security_review_rating": undefined, | |||
"reliability": undefined, | |||
"search": undefined, | |||
"security": undefined, | |||
"security_review_rating": undefined, | |||
"size": undefined, | |||
"sort": undefined, | |||
"tags": undefined, |
@@ -40,6 +40,16 @@ exports[`should render \`leak\` view correctly 1`] = ` | |||
} | |||
} | |||
/> | |||
<SecurityReviewFilter | |||
className="leak-facet-box" | |||
onQueryChange={[MockFunction]} | |||
property="new_security_review_rating" | |||
query={ | |||
Object { | |||
"view": "leak", | |||
} | |||
} | |||
/> | |||
<NewMaintainabilityFilter | |||
onQueryChange={[MockFunction]} | |||
query={ | |||
@@ -128,6 +138,14 @@ exports[`should render correctly 1`] = ` | |||
} | |||
} | |||
/> | |||
<SecurityReviewFilter | |||
onQueryChange={[MockFunction]} | |||
query={ | |||
Object { | |||
"size": "3", | |||
} | |||
} | |||
/> | |||
<MaintainabilityFilter | |||
onQueryChange={[MockFunction]} | |||
query={ |
@@ -64,6 +64,33 @@ exports[`should render correctly with all data 1`] = ` | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="project-card-measure" | |||
data-key="new_security_review_rating" | |||
> | |||
<div | |||
className="project-card-measure-inner" | |||
> | |||
<div | |||
className="project-card-measure-number" | |||
> | |||
<Measure | |||
className="spacer-right" | |||
metricKey="new_security_hotspots_reviewed" | |||
metricType="PERCENT" | |||
/> | |||
<Rating /> | |||
</div> | |||
<div | |||
className="project-card-measure-label-with-icon" | |||
> | |||
<SecurityHotspotIcon | |||
className="little-spacer-right text-bottom" | |||
/> | |||
metric.security_hotspots_reviewed.extra_short_name | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="project-card-measure" | |||
data-key="new_maintainability_rating" | |||
@@ -230,6 +257,33 @@ exports[`should render no data style new coverage, new duplications and new line | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="project-card-measure" | |||
data-key="new_security_review_rating" | |||
> | |||
<div | |||
className="project-card-measure-inner" | |||
> | |||
<div | |||
className="project-card-measure-number" | |||
> | |||
<Measure | |||
className="spacer-right" | |||
metricKey="new_security_hotspots_reviewed" | |||
metricType="PERCENT" | |||
/> | |||
<Rating /> | |||
</div> | |||
<div | |||
className="project-card-measure-label-with-icon" | |||
> | |||
<SecurityHotspotIcon | |||
className="little-spacer-right text-bottom" | |||
/> | |||
metric.security_hotspots_reviewed.extra_short_name | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="project-card-measure" | |||
data-key="new_maintainability_rating" |
@@ -64,6 +64,33 @@ exports[`should render correctly with all data 1`] = ` | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="project-card-measure" | |||
data-key="security_review_rating" | |||
> | |||
<div | |||
className="project-card-measure-inner" | |||
> | |||
<div | |||
className="project-card-measure-number" | |||
> | |||
<Measure | |||
className="spacer-right" | |||
metricKey="security_hotspots_reviewed" | |||
metricType="PERCENT" | |||
/> | |||
<Rating /> | |||
</div> | |||
<div | |||
className="project-card-measure-label-with-icon" | |||
> | |||
<SecurityHotspotIcon | |||
className="little-spacer-right text-bottom" | |||
/> | |||
metric.security_hotspots_reviewed.extra_short_name | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="project-card-measure" | |||
data-key="sqale_rating" |
@@ -19,10 +19,6 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { | |||
getCoverageRatingAverageValue, | |||
getCoverageRatingLabel | |||
} from 'sonar-ui-common/helpers/ratings'; | |||
import CoverageRating from '../../../components/ui/CoverageRating'; | |||
import { Facet } from '../types'; | |||
import Filter from './Filter'; | |||
@@ -39,6 +35,16 @@ export interface Props { | |||
value?: any; | |||
} | |||
const NO_DATA = 'NO_DATA'; | |||
const slices: { [key: string]: { label: string; average: number } } = { | |||
'80.0-*': { label: '≥ 80%', average: 90 }, | |||
'70.0-80.0': { label: '70% - 80%', average: 75 }, | |||
'50.0-70.0': { label: '50% - 70%', average: 60 }, | |||
'30.0-50.0': { label: '30% - 50%', average: 40 }, | |||
'*-30.0': { label: '< 30%', average: 15 }, | |||
[NO_DATA]: { label: '', average: NaN } | |||
}; | |||
export default function CoverageFilter(props: Props) { | |||
const { property = 'coverage' } = props; | |||
@@ -46,13 +52,12 @@ export default function CoverageFilter(props: Props) { | |||
<Filter | |||
className={props.className} | |||
facet={props.facet} | |||
getFacetValueForOption={getFacetValueForOption} | |||
header={<FilterHeader name={translate('metric_domain.Coverage')} />} | |||
highlightUnder={1} | |||
highlightUnderMax={5} | |||
maxFacetValue={props.maxFacetValue} | |||
onQueryChange={props.onQueryChange} | |||
options={[1, 2, 3, 4, 5, 6]} | |||
options={Object.keys(slices)} | |||
organization={props.organization} | |||
property={property} | |||
query={props.query} | |||
@@ -62,24 +67,15 @@ export default function CoverageFilter(props: Props) { | |||
); | |||
} | |||
function getFacetValueForOption(facet: Facet, option: number): number { | |||
const map = ['80.0-*', '70.0-80.0', '50.0-70.0', '30.0-50.0', '*-30.0', 'NO_DATA']; | |||
return facet[map[option - 1]]; | |||
} | |||
function renderOption(option: number, selected: boolean) { | |||
function renderOption(option: string, selected: boolean) { | |||
return ( | |||
<span> | |||
{option < 6 && ( | |||
<CoverageRating | |||
muted={!selected} | |||
size="small" | |||
value={getCoverageRatingAverageValue(option)} | |||
/> | |||
{option !== NO_DATA && ( | |||
<CoverageRating muted={!selected} size="small" value={slices[option].average} /> | |||
)} | |||
<span className="spacer-left"> | |||
{option < 6 ? ( | |||
getCoverageRatingLabel(option) | |||
{option !== NO_DATA ? ( | |||
slices[option].label | |||
) : ( | |||
<span className="big-spacer-left">{translate('no_data')}</span> | |||
)} |
@@ -49,6 +49,8 @@ interface Props { | |||
footer?: React.ReactNode; | |||
} | |||
const defaultGetFacetValueForOption = (facet: Facet, option: string | number) => facet[option]; | |||
export default class Filter extends React.PureComponent<Props> { | |||
isSelected(option: Option): boolean { | |||
const { value } = this.props; | |||
@@ -103,7 +105,7 @@ export default class Filter extends React.PureComponent<Props> { | |||
} | |||
renderOption(option: Option) { | |||
const { facet, getFacetValueForOption, value } = this.props; | |||
const { facet, getFacetValueForOption = defaultGetFacetValueForOption, value } = this.props; | |||
const className = classNames( | |||
'facet', | |||
'search-navigator-facet', |
@@ -42,7 +42,6 @@ export default function IssuesFilter(props: Props) { | |||
<Filter | |||
className={props.className} | |||
facet={props.facet} | |||
getFacetValueForOption={getFacetValueForOption} | |||
header={ | |||
<FilterHeader name={translate('metric_domain', props.name)}> | |||
{props.headerDetail} | |||
@@ -61,10 +60,6 @@ export default function IssuesFilter(props: Props) { | |||
); | |||
} | |||
function getFacetValueForOption(facet: Facet, option: number) { | |||
return facet[option]; | |||
} | |||
function renderOption(option: number, selected: boolean) { | |||
return ( | |||
<span> |
@@ -51,8 +51,6 @@ export default class LanguagesFilter extends React.Component<Props> { | |||
getSortedOptions = (facet: Facet = {}) => | |||
sortBy(Object.keys(facet), [(option: string) => -facet[option], (option: string) => option]); | |||
getFacetValueForOption = (facet: Facet = {}, option: string) => facet[option]; | |||
renderOption = (option: string) => ( | |||
<SearchableFilterOption | |||
option={getLanguageByKey(this.props.languages, option)} | |||
@@ -75,7 +73,6 @@ export default class LanguagesFilter extends React.Component<Props> { | |||
query={this.props.query} | |||
/> | |||
} | |||
getFacetValueForOption={this.getFacetValueForOption} | |||
header={<FilterHeader name={translate('projects.facets.languages')} />} | |||
maxFacetValue={this.props.maxFacetValue} | |||
onQueryChange={this.props.onQueryChange} |
@@ -40,7 +40,7 @@ export default function MaintainabilityFilter(props: Props) { | |||
{...props} | |||
headerDetail={ | |||
<span className="note little-spacer-left"> | |||
{'('} | |||
{'( '} | |||
<CodeSmellIcon className="little-spacer-right" /> | |||
{translate('metric.code_smells.name')} | |||
{' )'} |
@@ -40,7 +40,7 @@ export default function NewMaintainabilityFilter(props: Props) { | |||
className="leak-facet-box" | |||
headerDetail={ | |||
<span className="note little-spacer-left"> | |||
{'('} | |||
{'( '} | |||
<CodeSmellIcon className="little-spacer-right" /> | |||
{translate('metric.code_smells.name')} | |||
{' )'} |
@@ -40,7 +40,7 @@ export default function NewReliabilityFilter(props: Props) { | |||
className="leak-facet-box" | |||
headerDetail={ | |||
<span className="note little-spacer-left"> | |||
{'('} | |||
{'( '} | |||
<BugIcon className="little-spacer-right" /> | |||
{translate('metric.bugs.name')} | |||
{' )'} |
@@ -40,7 +40,7 @@ export default function NewSecurityFilter(props: Props) { | |||
className="leak-facet-box" | |||
headerDetail={ | |||
<span className="note little-spacer-left"> | |||
{'('} | |||
{'( '} | |||
<VulnerabilityIcon className="little-spacer-right" /> | |||
{translate('metric.vulnerabilities.name')} | |||
{' )'} |
@@ -42,7 +42,6 @@ export default function QualityGateFilter(props: Props) { | |||
return ( | |||
<Filter | |||
facet={props.facet} | |||
getFacetValueForOption={getFacetValueForOption} | |||
header={<FilterHeader name={translate('projects.facets.quality_gate')} />} | |||
maxFacetValue={props.maxFacetValue} | |||
onQueryChange={props.onQueryChange} | |||
@@ -56,10 +55,6 @@ export default function QualityGateFilter(props: Props) { | |||
); | |||
} | |||
function getFacetValueForOption(facet: Facet, option: string) { | |||
return facet[option]; | |||
} | |||
function renderOption(option: string, selected: boolean) { | |||
return ( | |||
<> |
@@ -40,7 +40,7 @@ export default function ReliabilityFilter(props: Props) { | |||
{...props} | |||
headerDetail={ | |||
<span className="note little-spacer-left"> | |||
{'('} | |||
{'( '} | |||
<BugIcon className="little-spacer-right" /> | |||
{translate('metric.bugs.name')} | |||
{' )'} |
@@ -40,7 +40,7 @@ export default function SecurityFilter(props: Props) { | |||
{...props} | |||
headerDetail={ | |||
<span className="note little-spacer-left"> | |||
{'('} | |||
{'( '} | |||
<VulnerabilityIcon className="little-spacer-right" /> | |||
{translate('metric.vulnerabilities.name')} | |||
{' )'} |
@@ -0,0 +1,86 @@ | |||
/* | |||
* 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 SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon'; | |||
import Rating from 'sonar-ui-common/components/ui/Rating'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { Facet } from '../types'; | |||
import Filter from './Filter'; | |||
import FilterHeader from './FilterHeader'; | |||
export interface Props { | |||
className?: string; | |||
facet?: Facet; | |||
maxFacetValue?: number; | |||
onQueryChange: (change: T.RawQuery) => void; | |||
organization?: { key: string }; | |||
property?: string; | |||
query: T.Dict<any>; | |||
value?: any; | |||
} | |||
const labels: T.Dict<string> = { | |||
1: '≥ 80%', | |||
2: '70% - 80%', | |||
3: '50% - 70%', | |||
4: '30% - 50%', | |||
5: '< 30%' | |||
}; | |||
export default function SecurityReviewFilter(props: Props) { | |||
const { property = 'security_review' } = props; | |||
return ( | |||
<Filter | |||
className={props.className} | |||
facet={props.facet} | |||
header={ | |||
<FilterHeader name={translate('metric_domain.SecurityReview')}> | |||
{ | |||
<span className="note little-spacer-left"> | |||
{'( '} | |||
<SecurityHotspotIcon className="little-spacer-right" /> | |||
{translate('metric.security_hotspots.name')} | |||
{' )'} | |||
</span> | |||
} | |||
</FilterHeader> | |||
} | |||
highlightUnder={1} | |||
maxFacetValue={props.maxFacetValue} | |||
onQueryChange={props.onQueryChange} | |||
options={[1, 2, 3, 4, 5]} | |||
organization={props.organization} | |||
property={property} | |||
query={props.query} | |||
renderOption={renderOption} | |||
value={props.value} | |||
/> | |||
); | |||
} | |||
function renderOption(option: number, selected: boolean) { | |||
return ( | |||
<span> | |||
<Rating muted={!selected} small={true} value={option} /> | |||
<span className="spacer-left">{labels[option]}</span> | |||
</span> | |||
); | |||
} |
@@ -95,8 +95,6 @@ export default class TagsFilter extends React.PureComponent<Props, State> { | |||
getSortedOptions = (facet: Facet = {}) => | |||
sortBy(Object.keys(facet), [(option: string) => -facet[option], (option: string) => option]); | |||
getFacetValueForOption = (facet: Facet = {}, option: string) => facet[option]; | |||
renderOption = (option: string) => <SearchableFilterOption optionKey={option} />; | |||
render() { | |||
@@ -117,7 +115,6 @@ export default class TagsFilter extends React.PureComponent<Props, State> { | |||
query={this.props.query} | |||
/> | |||
} | |||
getFacetValueForOption={this.getFacetValueForOption} | |||
header={<FilterHeader name={translate('projects.facets.tags')} />} | |||
maxFacetValue={this.props.maxFacetValue} | |||
onQueryChange={this.props.onQueryChange} |
@@ -26,13 +26,5 @@ it('renders', () => { | |||
expect(wrapper).toMatchSnapshot(); | |||
const renderOption = wrapper.prop('renderOption'); | |||
expect(renderOption(2, false)).toMatchSnapshot(); | |||
const getFacetValueForOption = wrapper.prop('getFacetValueForOption'); | |||
expect( | |||
getFacetValueForOption( | |||
{ '80.0-*': 1, '70.0-80.0': 42, '50.0-70.0': 14, '30.0-50.0': 13, '*-30.0': 8, NO_DATA: 3 }, | |||
2 | |||
) | |||
).toBe(42); | |||
expect(renderOption('70.0-80.0', false)).toMatchSnapshot(); | |||
}); |
@@ -29,7 +29,4 @@ it('renders', () => { | |||
const renderOption = wrapper.prop('renderOption'); | |||
expect(renderOption(2, false)).toMatchSnapshot(); | |||
const getFacetValueForOption = wrapper.prop('getFacetValueForOption'); | |||
expect(getFacetValueForOption({ 1: 1, 2: 2, 3: 3, 4: 4, 5: 5 }, 2)).toBe(2); | |||
}); |
@@ -27,9 +27,6 @@ it('renders', () => { | |||
const renderOption = wrapper.prop('renderOption'); | |||
expect(renderOption(2, false)).toMatchSnapshot(); | |||
const getFacetValueForOption = wrapper.prop('getFacetValueForOption'); | |||
expect(getFacetValueForOption({ ERROR: 1, OK: 3 }, 'OK')).toBe(3); | |||
}); | |||
it('should render with warning facet', () => { |
@@ -0,0 +1,30 @@ | |||
/* | |||
* 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 SecurityReviewFilter from '../SecurityReviewFilter'; | |||
it('renders', () => { | |||
const wrapper = shallow(<SecurityReviewFilter onQueryChange={jest.fn()} query={{}} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
const renderOption = wrapper.prop('renderOption'); | |||
expect(renderOption(2, false)).toMatchSnapshot('option'); | |||
}); |
@@ -2,7 +2,6 @@ | |||
exports[`renders 1`] = ` | |||
<Filter | |||
getFacetValueForOption={[Function]} | |||
header={ | |||
<FilterHeader | |||
name="metric_domain.Coverage" | |||
@@ -13,12 +12,12 @@ exports[`renders 1`] = ` | |||
onQueryChange={[MockFunction]} | |||
options={ | |||
Array [ | |||
1, | |||
2, | |||
3, | |||
4, | |||
5, | |||
6, | |||
"80.0-*", | |||
"70.0-80.0", | |||
"50.0-70.0", | |||
"30.0-50.0", | |||
"*-30.0", | |||
"NO_DATA", | |||
] | |||
} | |||
property="coverage" |
@@ -2,7 +2,6 @@ | |||
exports[`renders 1`] = ` | |||
<Filter | |||
getFacetValueForOption={[Function]} | |||
header={ | |||
<FilterHeader | |||
name="metric_domain.bugs" |
@@ -39,7 +39,6 @@ exports[`should render the languages facet with the selected languages 1`] = ` | |||
} | |||
/> | |||
} | |||
getFacetValueForOption={[Function]} | |||
header={ | |||
<FilterHeader | |||
name="projects.facets.languages" | |||
@@ -229,7 +228,6 @@ exports[`should render the languages without the ones in the facet 1`] = ` | |||
} | |||
/> | |||
} | |||
getFacetValueForOption={[Function]} | |||
header={ | |||
<FilterHeader | |||
name="projects.facets.languages" |
@@ -6,7 +6,7 @@ exports[`renders 1`] = ` | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
( | |||
( | |||
<CodeSmellIcon | |||
className="little-spacer-right" | |||
/> |
@@ -7,7 +7,7 @@ exports[`renders 1`] = ` | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
( | |||
( | |||
<CodeSmellIcon | |||
className="little-spacer-right" | |||
/> |
@@ -7,7 +7,7 @@ exports[`renders 1`] = ` | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
( | |||
( | |||
<BugIcon | |||
className="little-spacer-right" | |||
/> |
@@ -7,7 +7,7 @@ exports[`renders 1`] = ` | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
( | |||
( | |||
<VulnerabilityIcon | |||
className="little-spacer-right" | |||
/> |
@@ -2,7 +2,6 @@ | |||
exports[`renders 1`] = ` | |||
<Filter | |||
getFacetValueForOption={[Function]} | |||
header={ | |||
<FilterHeader | |||
name="projects.facets.quality_gate" |
@@ -6,7 +6,7 @@ exports[`renders 1`] = ` | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
( | |||
( | |||
<BugIcon | |||
className="little-spacer-right" | |||
/> |
@@ -6,7 +6,7 @@ exports[`renders 1`] = ` | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
( | |||
( | |||
<VulnerabilityIcon | |||
className="little-spacer-right" | |||
/> |
@@ -0,0 +1,51 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<Filter | |||
header={ | |||
<FilterHeader | |||
name="metric_domain.SecurityReview" | |||
> | |||
<span | |||
className="note little-spacer-left" | |||
> | |||
( | |||
<SecurityHotspotIcon | |||
className="little-spacer-right" | |||
/> | |||
metric.security_hotspots.name | |||
) | |||
</span> | |||
</FilterHeader> | |||
} | |||
highlightUnder={1} | |||
onQueryChange={[MockFunction]} | |||
options={ | |||
Array [ | |||
1, | |||
2, | |||
3, | |||
4, | |||
5, | |||
] | |||
} | |||
property="security_review" | |||
query={Object {}} | |||
renderOption={[Function]} | |||
/> | |||
`; | |||
exports[`renders: option 1`] = ` | |||
<span> | |||
<Rating | |||
muted={true} | |||
small={true} | |||
value={2} | |||
/> | |||
<span | |||
className="spacer-left" | |||
> | |||
70% - 80% | |||
</span> | |||
</span> | |||
`; |
@@ -71,7 +71,6 @@ exports[`should render maximum 10 tags in the searchbox results 1`] = ` | |||
} | |||
/> | |||
} | |||
getFacetValueForOption={[Function]} | |||
header={ | |||
<FilterHeader | |||
name="projects.facets.tags" | |||
@@ -132,7 +131,6 @@ exports[`should render the tags facet with the selected tags 1`] = ` | |||
} | |||
/> | |||
} | |||
getFacetValueForOption={[Function]} | |||
header={ | |||
<FilterHeader | |||
name="projects.facets.tags" | |||
@@ -280,7 +278,6 @@ exports[`should render the tags without the ones in the facet 1`] = ` | |||
} | |||
/> | |||
} | |||
getFacetValueForOption={[Function]} | |||
header={ | |||
<FilterHeader | |||
name="projects.facets.tags" | |||
@@ -343,7 +340,6 @@ exports[`should render the tags without the ones in the facet 2`] = ` | |||
} | |||
/> | |||
} | |||
getFacetValueForOption={[Function]} | |||
header={ | |||
<FilterHeader | |||
name="projects.facets.tags" |
@@ -27,6 +27,8 @@ export interface Query { | |||
new_reliability?: number; | |||
security?: number; | |||
new_security?: number; | |||
security_review_rating?: number; | |||
new_security_review_rating?: number; | |||
maintainability?: number; | |||
new_maintainability?: number; | |||
coverage?: number; | |||
@@ -51,6 +53,8 @@ export function parseUrlQuery(urlQuery: T.RawQuery): Query { | |||
new_reliability: getAsNumericRating(urlQuery['new_reliability']), | |||
security: getAsNumericRating(urlQuery['security']), | |||
new_security: getAsNumericRating(urlQuery['new_security']), | |||
security_review_rating: getAsNumericRating(urlQuery['security_review']), | |||
new_security_review_rating: getAsNumericRating(urlQuery['new_security_review']), | |||
maintainability: getAsNumericRating(urlQuery['maintainability']), | |||
new_maintainability: getAsNumericRating(urlQuery['new_maintainability']), | |||
coverage: getAsNumericRating(urlQuery['coverage']), | |||
@@ -94,9 +98,11 @@ export function convertToFilter(query: Query, isFavorite: boolean): string { | |||
[ | |||
'reliability', | |||
'security', | |||
'security_review_rating', | |||
'maintainability', | |||
'new_reliability', | |||
'new_security', | |||
'new_security_review_rating', | |||
'new_maintainability' | |||
].forEach(property => pushMetricToArray(query, property, conditions, convertIssuesRating)); | |||
@@ -232,6 +238,8 @@ function mapPropertyToMetric(property?: string): string | undefined { | |||
new_reliability: 'new_reliability_rating', | |||
security: 'security_rating', | |||
new_security: 'new_security_rating', | |||
security_review_rating: 'security_review_rating', | |||
new_security_review_rating: 'new_security_review_rating', | |||
maintainability: 'sqale_rating', | |||
new_maintainability: 'new_maintainability_rating', | |||
coverage: 'coverage', |
@@ -93,7 +93,7 @@ | |||
.project-card-measures { | |||
display: flex; | |||
padding-top: 8px; | |||
margin: 0 -15px; | |||
margin: 0 -15px 0 -30px; | |||
} | |||
.project-card-leak-measures { | |||
@@ -114,7 +114,7 @@ | |||
} | |||
.project-card-measure { | |||
width: 130px; | |||
flex: 0 1 15%; | |||
} | |||
.project-card-measure { | |||
@@ -123,7 +123,7 @@ | |||
vertical-align: top; | |||
text-align: center; | |||
box-sizing: border-box; | |||
padding: 0 15px; | |||
padding: 0 var(--gridSize); | |||
} | |||
.project-card-measure + .project-card-measure:before { |
@@ -17,7 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { uniq } from 'lodash'; | |||
import { invert, uniq } from 'lodash'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { RequestData } from 'sonar-ui-common/helpers/request'; | |||
import { Facet, searchProjects } from '../../api/components'; | |||
@@ -97,6 +97,8 @@ const METRICS = [ | |||
'reliability_rating', | |||
'vulnerabilities', | |||
'security_rating', | |||
'security_hotspots_reviewed', | |||
'security_review_rating', | |||
'code_smells', | |||
'sqale_rating', | |||
'duplicated_lines_density', | |||
@@ -111,6 +113,8 @@ const LEAK_METRICS = [ | |||
'new_reliability_rating', | |||
'new_vulnerabilities', | |||
'new_security_rating', | |||
'new_security_hotspots_reviewed', | |||
'new_security_review_rating', | |||
'new_code_smells', | |||
'new_maintainability_rating', | |||
'new_coverage', | |||
@@ -131,6 +135,7 @@ const METRICS_BY_VISUALIZATION: T.Dict<string[]> = { | |||
export const FACETS = [ | |||
'reliability_rating', | |||
'security_rating', | |||
'security_review_rating', | |||
'sqale_rating', | |||
'coverage', | |||
'duplicated_lines_density', | |||
@@ -143,6 +148,7 @@ export const FACETS = [ | |||
export const LEAK_FACETS = [ | |||
'new_reliability_rating', | |||
'new_security_rating', | |||
'new_security_review_rating', | |||
'new_maintainability_rating', | |||
'new_coverage', | |||
'new_duplicated_lines_density', | |||
@@ -284,10 +290,34 @@ function mapFacetValues(values: Array<{ val: string; count: number }>) { | |||
return map; | |||
} | |||
const propertyToMetricMap: T.Dict<string | undefined> = { | |||
analysis_date: 'analysisDate', | |||
reliability: 'reliability_rating', | |||
new_reliability: 'new_reliability_rating', | |||
security: 'security_rating', | |||
new_security: 'new_security_rating', | |||
security_review_rating: 'security_review_rating', | |||
new_security_review_rating: 'new_security_review_rating', | |||
maintainability: 'sqale_rating', | |||
new_maintainability: 'new_maintainability_rating', | |||
coverage: 'coverage', | |||
new_coverage: 'new_coverage', | |||
duplications: 'duplicated_lines_density', | |||
new_duplications: 'new_duplicated_lines_density', | |||
size: 'ncloc', | |||
new_lines: 'new_lines', | |||
gate: 'alert_status', | |||
languages: 'languages', | |||
tags: 'tags', | |||
search: 'query' | |||
}; | |||
const metricToPropertyMap = invert(propertyToMetricMap); | |||
function getFacetsMap(facets: Facet[]) { | |||
const map: T.Dict<T.Dict<number>> = {}; | |||
facets.forEach(facet => { | |||
const property = mapMetricToProperty(facet.property); | |||
const property = metricToPropertyMap[facet.property]; | |||
const { values } = facet; | |||
if (REVERSED_FACETS.includes(property)) { | |||
values.reverse(); | |||
@@ -297,57 +327,11 @@ function getFacetsMap(facets: Facet[]) { | |||
return map; | |||
} | |||
function mapPropertyToMetric(property?: string) { | |||
const map: T.Dict<string> = { | |||
analysis_date: 'analysisDate', | |||
reliability: 'reliability_rating', | |||
new_reliability: 'new_reliability_rating', | |||
security: 'security_rating', | |||
new_security: 'new_security_rating', | |||
maintainability: 'sqale_rating', | |||
new_maintainability: 'new_maintainability_rating', | |||
coverage: 'coverage', | |||
new_coverage: 'new_coverage', | |||
duplications: 'duplicated_lines_density', | |||
new_duplications: 'new_duplicated_lines_density', | |||
size: 'ncloc', | |||
new_lines: 'new_lines', | |||
gate: 'alert_status', | |||
languages: 'languages', | |||
tags: 'tags', | |||
search: 'query' | |||
}; | |||
return property && map[property]; | |||
} | |||
function convertToSorting({ sort }: Query): { s?: string; asc?: boolean } { | |||
if (sort && sort[0] === '-') { | |||
return { s: mapPropertyToMetric(sort.substr(1)), asc: false }; | |||
return { s: propertyToMetricMap[sort.substr(1)], asc: false }; | |||
} | |||
return { s: mapPropertyToMetric(sort) }; | |||
} | |||
function mapMetricToProperty(metricKey: string) { | |||
const map: T.Dict<string> = { | |||
analysisDate: 'analysis_date', | |||
reliability_rating: 'reliability', | |||
new_reliability_rating: 'new_reliability', | |||
security_rating: 'security', | |||
new_security_rating: 'new_security', | |||
sqale_rating: 'maintainability', | |||
new_maintainability_rating: 'new_maintainability', | |||
coverage: 'coverage', | |||
new_coverage: 'new_coverage', | |||
duplicated_lines_density: 'duplications', | |||
new_duplicated_lines_density: 'new_duplications', | |||
ncloc: 'size', | |||
new_lines: 'new_lines', | |||
alert_status: 'gate', | |||
languages: 'languages', | |||
tags: 'tags', | |||
query: 'search' | |||
}; | |||
return map[metricKey]; | |||
return { s: propertyToMetricMap[sort || ''] }; | |||
} | |||
const ONE_MINUTE = 60000; |
@@ -2111,7 +2111,7 @@ metric.new_reliability_remediation_effort.extra_short_name=Remediation Effort | |||
metric.new_security_hotspots.description=New Security Hotspots | |||
metric.new_security_hotspots.name=New Security Hotspots | |||
metric.new_security_hotspots.short_name=Security Hotspots | |||
metric.new_security_hotspots_reviewed.description=Security Hotspots Reviewed on New Code | |||
metric.new_security_hotspots_reviewed.description=Percentage of Security Hotspots Reviewed on New Code | |||
metric.new_security_hotspots_reviewed.name=Security Hotspots Reviewed on New Code | |||
metric.new_security_hotspots_reviewed.short_name=Security Hotspots Reviewed | |||
metric.new_security_rating.description=Security rating on new code | |||
@@ -2221,7 +2221,7 @@ metric.rfc_distribution.description=Class distribution /RFC | |||
metric.rfc_distribution.name=Class Distribution / RFC | |||
metric.security_hotspots.description=Security Hotspots | |||
metric.security_hotspots.name=Security Hotspots | |||
metric.security_hotspots_reviewed.description=Security Hotspots Reviewed | |||
metric.security_hotspots_reviewed.description=Percentage of Security Hotspots Reviewed | |||
metric.security_hotspots_reviewed.name=Security Hotspots Reviewed | |||
metric.security_hotspots_reviewed.extra_short_name=Hotspots Reviewed | |||
metric.security_rating.description=Security rating |
@@ -1563,7 +1563,7 @@ public final class CoreMetrics { | |||
* @since 8.2 | |||
*/ | |||
public static final Metric<Integer> SECURITY_HOTSPOTS_REVIEWED = new Metric.Builder(SECURITY_HOTSPOTS_REVIEWED_KEY, "Security Hotspots Reviewed", Metric.ValueType.PERCENT) | |||
.setDescription("Security Hotspots Reviewed") | |||
.setDescription("Percentage of Security Hotspots Reviewed") | |||
.setDomain(DOMAIN_SECURITY_REVIEW) | |||
.setDirection(Metric.DIRECTION_BETTER) | |||
.setQualitative(true) | |||
@@ -1581,7 +1581,7 @@ public final class CoreMetrics { | |||
*/ | |||
public static final Metric<Integer> NEW_SECURITY_HOTSPOTS_REVIEWED = new Metric.Builder(NEW_SECURITY_HOTSPOTS_REVIEWED_KEY, "Security Hotspots Reviewed on New Code", | |||
Metric.ValueType.PERCENT) | |||
.setDescription("Security Hotspots Reviewed on New Code") | |||
.setDescription("Percentage of Security Hotspots Reviewed on New Code") | |||
.setDomain(DOMAIN_SECURITY_REVIEW) | |||
.setDirection(Metric.DIRECTION_BETTER) | |||
.setDeleteHistoricalData(true) |