@@ -68,3 +68,4 @@ scripts/patches/*license*.txt | |||
!scripts/patches/debug_ce.sh | |||
!scripts/patches/debug_web.sh | |||
!scripts/patches/postgres.sh | |||
gherkin-features/ |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; | |||
import { collapsePath, limitComponentName } from 'sonar-ui-common/helpers/path'; | |||
import Organization from '../../../components/shared/Organization'; | |||
import { getSelectedLocation } from '../utils'; | |||
@@ -28,6 +29,7 @@ interface Props { | |||
T.Issue, | |||
| 'component' | |||
| 'componentLongName' | |||
| 'componentQualifier' | |||
| 'flows' | |||
| 'organization' | |||
| 'project' | |||
@@ -59,6 +61,8 @@ export default function ComponentBreadcrumbs({ | |||
return ( | |||
<div className="component-name text-ellipsis"> | |||
<QualifierIcon className="spacer-right" qualifier={issue.componentQualifier} /> | |||
{displayOrganization && <Organization link={false} organizationKey={issue.organization} />} | |||
{displayProject && ( |
@@ -19,11 +19,13 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import ComponentBreadcrumbs from '../ComponentBreadcrumbs'; | |||
const baseIssue = { | |||
component: 'comp', | |||
componentLongName: 'comp-name', | |||
componentQualifier: ComponentQualifier.File, | |||
flows: [], | |||
organization: 'org', | |||
project: 'proj', |
@@ -4,6 +4,10 @@ exports[`renders 1`] = ` | |||
<div | |||
className="component-name text-ellipsis" | |||
> | |||
<QualifierIcon | |||
className="spacer-right" | |||
qualifier="FIL" | |||
/> | |||
<Connect(Organization) | |||
link={false} | |||
organizationKey="org" | |||
@@ -28,6 +32,10 @@ exports[`renders with sub-project 1`] = ` | |||
<div | |||
className="component-name text-ellipsis" | |||
> | |||
<QualifierIcon | |||
className="spacer-right" | |||
qualifier="FIL" | |||
/> | |||
<Connect(Organization) | |||
link={false} | |||
organizationKey="org" |
@@ -0,0 +1,96 @@ | |||
/* | |||
* 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 { without } from 'lodash'; | |||
import * as React from 'react'; | |||
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import FacetBox from '../../../components/facet/FacetBox'; | |||
import FacetHeader from '../../../components/facet/FacetHeader'; | |||
import FacetItem from '../../../components/facet/FacetItem'; | |||
import FacetItemsList from '../../../components/facet/FacetItemsList'; | |||
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; | |||
import { SOURCE_SCOPES } from '../../../helpers/constants'; | |||
import { formatFacetStat, Query } from '../utils'; | |||
export interface ScopeFacetProps { | |||
fetching: boolean; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
scopes: string[]; | |||
stats: T.Dict<number> | undefined; | |||
} | |||
export default function ScopeFacet(props: ScopeFacetProps) { | |||
const { fetching, open, scopes = [], stats = {} } = props; | |||
const values = scopes.map(scope => translate('issue.scope', scope)); | |||
return ( | |||
<FacetBox property="scopes"> | |||
<FacetHeader | |||
fetching={fetching} | |||
name={translate('issues.facet.scopes')} | |||
onClear={() => props.onChange({ scopes: [] })} | |||
onClick={() => props.onToggle('scopes')} | |||
open={open} | |||
values={values} | |||
/> | |||
{open && ( | |||
<> | |||
<FacetItemsList> | |||
{SOURCE_SCOPES.map(({ scope, qualifier }) => { | |||
const active = scopes.includes(scope); | |||
const stat = stats[scope]; | |||
return ( | |||
<FacetItem | |||
active={active} | |||
disabled={stat === 0 && !active} | |||
key={scope} | |||
name={ | |||
<span className="display-flex-center"> | |||
<QualifierIcon className="little-spacer-right" qualifier={qualifier} />{' '} | |||
{translate('issue.scope', scope)} | |||
</span> | |||
} | |||
onClick={(itemValue: string, multiple: boolean) => { | |||
if (multiple) { | |||
props.onChange({ | |||
scopes: active ? without(scopes, itemValue) : [...scopes, itemValue] | |||
}); | |||
} else { | |||
props.onChange({ | |||
scopes: active && scopes.length === 1 ? [] : [itemValue] | |||
}); | |||
} | |||
}} | |||
stat={formatFacetStat(stat)} | |||
value={scope} | |||
/> | |||
); | |||
})} | |||
</FacetItemsList> | |||
<MultipleSelectionHint options={Object.keys(stats).length} values={scopes.length} /> | |||
</> | |||
)} | |||
</FacetBox> | |||
); | |||
} |
@@ -32,6 +32,7 @@ import LanguageFacet from './LanguageFacet'; | |||
import ProjectFacet from './ProjectFacet'; | |||
import ResolutionFacet from './ResolutionFacet'; | |||
import RuleFacet from './RuleFacet'; | |||
import ScopeFacet from './ScopeFacet'; | |||
import SeverityFacet from './SeverityFacet'; | |||
import StandardFacet from './StandardFacet'; | |||
import StatusFacet from './StatusFacet'; | |||
@@ -126,6 +127,14 @@ export class Sidebar extends React.PureComponent<Props> { | |||
severities={query.severities} | |||
stats={facets.severities} | |||
/> | |||
<ScopeFacet | |||
fetching={this.props.loadingFacets.scopes === true} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.scopes} | |||
stats={facets.scopes} | |||
scopes={query.scopes} | |||
/> | |||
<ResolutionFacet | |||
fetching={this.props.loadingFacets.resolutions === true} | |||
onChange={this.props.onFilterChange} |
@@ -0,0 +1,94 @@ | |||
/* | |||
* 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, ShallowWrapper } from 'enzyme'; | |||
import * as React from 'react'; | |||
import FacetHeader from '../../../../components/facet/FacetHeader'; | |||
import FacetItem from '../../../../components/facet/FacetItem'; | |||
import { IssueScope } from '../../../../types/issues'; | |||
import ScopeFacet, { ScopeFacetProps } from '../ScopeFacet'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ open: true })).toMatchSnapshot('open'); | |||
expect(shallowRender({ open: true, scopes: [IssueScope.Main] })).toMatchSnapshot('active facet'); | |||
expect(shallowRender({ open: true, stats: { [IssueScope.Main]: 0 } })).toMatchSnapshot( | |||
'disabled facet' | |||
); | |||
}); | |||
it('should correctly handle facet header clicks', () => { | |||
const onChange = jest.fn(); | |||
const onToggle = jest.fn(); | |||
const wrapper = shallowRender({ onChange, onToggle }); | |||
wrapper.find(FacetHeader).props().onClear!(); | |||
expect(onChange).toBeCalledWith({ scopes: [] }); | |||
wrapper.find(FacetHeader).props().onClick!(); | |||
expect(onToggle).toBeCalledWith('scopes'); | |||
}); | |||
it('should correctly handle facet item clicks', () => { | |||
const wrapper = shallowRender({ open: true, scopes: [IssueScope.Main] }); | |||
const onChange = jest.fn(({ scopes }) => wrapper.setProps({ scopes })); | |||
wrapper.setProps({ onChange }); | |||
clickFacetItem(wrapper, IssueScope.Test); | |||
expect(onChange).toHaveBeenLastCalledWith({ scopes: [IssueScope.Test] }); | |||
clickFacetItem(wrapper, IssueScope.Test); | |||
expect(onChange).toHaveBeenLastCalledWith({ scopes: [] }); | |||
clickFacetItem(wrapper, IssueScope.Test, true); | |||
clickFacetItem(wrapper, IssueScope.Main, true); | |||
expect(onChange).toHaveBeenLastCalledWith({ | |||
scopes: expect.arrayContaining([IssueScope.Main, IssueScope.Test]) | |||
}); | |||
clickFacetItem(wrapper, IssueScope.Test, true); | |||
expect(onChange).toHaveBeenLastCalledWith({ scopes: [IssueScope.Main] }); | |||
}); | |||
function clickFacetItem( | |||
wrapper: ShallowWrapper<ScopeFacetProps>, | |||
scope: IssueScope, | |||
multiple = false | |||
) { | |||
return wrapper | |||
.find(FacetItem) | |||
.filterWhere(f => f.key() === scope) | |||
.props() | |||
.onClick(scope, multiple); | |||
} | |||
function shallowRender(props: Partial<ScopeFacetProps> = {}) { | |||
return shallow<ScopeFacetProps>( | |||
<ScopeFacet | |||
fetching={true} | |||
onChange={jest.fn()} | |||
onToggle={jest.fn()} | |||
open={false} | |||
scopes={[]} | |||
stats={{}} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,210 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: active facet 1`] = ` | |||
<FacetBox | |||
property="scopes" | |||
> | |||
<FacetHeader | |||
fetching={true} | |||
name="issues.facet.scopes" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={ | |||
Array [ | |||
"issue.scope.MAIN", | |||
] | |||
} | |||
/> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={true} | |||
disabled={false} | |||
halfWidth={false} | |||
key="MAIN" | |||
loading={false} | |||
name={ | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="FIL" | |||
/> | |||
issue.scope.MAIN | |||
</span> | |||
} | |||
onClick={[Function]} | |||
value="MAIN" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="TEST" | |||
loading={false} | |||
name={ | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="UTS" | |||
/> | |||
issue.scope.TEST | |||
</span> | |||
} | |||
onClick={[Function]} | |||
value="TEST" | |||
/> | |||
</FacetItemsList> | |||
<MultipleSelectionHint | |||
options={0} | |||
values={1} | |||
/> | |||
</FacetBox> | |||
`; | |||
exports[`should render correctly: default 1`] = ` | |||
<FacetBox | |||
property="scopes" | |||
> | |||
<FacetHeader | |||
fetching={true} | |||
name="issues.facet.scopes" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={false} | |||
values={Array []} | |||
/> | |||
</FacetBox> | |||
`; | |||
exports[`should render correctly: disabled facet 1`] = ` | |||
<FacetBox | |||
property="scopes" | |||
> | |||
<FacetHeader | |||
fetching={true} | |||
name="issues.facet.scopes" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={Array []} | |||
/> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={false} | |||
disabled={true} | |||
halfWidth={false} | |||
key="MAIN" | |||
loading={false} | |||
name={ | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="FIL" | |||
/> | |||
issue.scope.MAIN | |||
</span> | |||
} | |||
onClick={[Function]} | |||
stat={0} | |||
value="MAIN" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="TEST" | |||
loading={false} | |||
name={ | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="UTS" | |||
/> | |||
issue.scope.TEST | |||
</span> | |||
} | |||
onClick={[Function]} | |||
value="TEST" | |||
/> | |||
</FacetItemsList> | |||
<MultipleSelectionHint | |||
options={1} | |||
values={0} | |||
/> | |||
</FacetBox> | |||
`; | |||
exports[`should render correctly: open 1`] = ` | |||
<FacetBox | |||
property="scopes" | |||
> | |||
<FacetHeader | |||
fetching={true} | |||
name="issues.facet.scopes" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={Array []} | |||
/> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="MAIN" | |||
loading={false} | |||
name={ | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="FIL" | |||
/> | |||
issue.scope.MAIN | |||
</span> | |||
} | |||
onClick={[Function]} | |||
value="MAIN" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="TEST" | |||
loading={false} | |||
name={ | |||
<span | |||
className="display-flex-center" | |||
> | |||
<QualifierIcon | |||
className="little-spacer-right" | |||
qualifier="UTS" | |||
/> | |||
issue.scope.TEST | |||
</span> | |||
} | |||
onClick={[Function]} | |||
value="TEST" | |||
/> | |||
</FacetItemsList> | |||
<MultipleSelectionHint | |||
options={0} | |||
values={0} | |||
/> | |||
</FacetBox> | |||
`; |
@@ -4,6 +4,7 @@ exports[`should not render developer nominative facets when asked not to 1`] = ` | |||
Array [ | |||
"TypeFacet", | |||
"SeverityFacet", | |||
"ScopeFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"StandardFacet", | |||
@@ -19,6 +20,7 @@ exports[`should render facets for developer 1`] = ` | |||
Array [ | |||
"TypeFacet", | |||
"SeverityFacet", | |||
"ScopeFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"StandardFacet", | |||
@@ -37,6 +39,7 @@ exports[`should render facets for directory 1`] = ` | |||
Array [ | |||
"TypeFacet", | |||
"SeverityFacet", | |||
"ScopeFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"StandardFacet", | |||
@@ -54,6 +57,7 @@ exports[`should render facets for global page 1`] = ` | |||
Array [ | |||
"TypeFacet", | |||
"SeverityFacet", | |||
"ScopeFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"StandardFacet", | |||
@@ -71,6 +75,7 @@ exports[`should render facets for module 1`] = ` | |||
Array [ | |||
"TypeFacet", | |||
"SeverityFacet", | |||
"ScopeFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"StandardFacet", | |||
@@ -89,6 +94,7 @@ exports[`should render facets for project 1`] = ` | |||
Array [ | |||
"TypeFacet", | |||
"SeverityFacet", | |||
"ScopeFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"StandardFacet", | |||
@@ -107,6 +113,7 @@ exports[`should render facets when my issues are selected 1`] = ` | |||
Array [ | |||
"TypeFacet", | |||
"SeverityFacet", | |||
"ScopeFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"StandardFacet", |
@@ -291,7 +291,7 @@ | |||
} | |||
.issues-workspace-list-component { | |||
padding: 10px 10px 6px; | |||
padding: 10px 0 6px; | |||
} | |||
.issues-workspace-list-item + .issues-workspace-list-item { |
@@ -54,6 +54,7 @@ export interface Query { | |||
resolved: boolean; | |||
rules: string[]; | |||
sansTop25: string[]; | |||
scopes: string[]; | |||
severities: string[]; | |||
sinceLeakPeriod: boolean; | |||
sonarsourceSecurity: string[]; | |||
@@ -96,6 +97,7 @@ export function parseQuery(query: T.RawQuery): Query { | |||
resolved: parseAsBoolean(query.resolved), | |||
rules: parseAsArray(query.rules, parseAsString), | |||
sansTop25: parseAsArray(query.sansTop25, parseAsString), | |||
scopes: parseAsArray(query.scopes, parseAsString), | |||
severities: parseAsArray(query.severities, parseAsString), | |||
sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false), | |||
sonarsourceSecurity: parseAsArray(query.sonarsourceSecurity, parseAsString), | |||
@@ -134,6 +136,7 @@ export function serializeQuery(query: Query): T.RawQuery { | |||
rules: serializeStringArray(query.rules), | |||
s: serializeString(query.sort), | |||
sansTop25: serializeStringArray(query.sansTop25), | |||
scopes: serializeStringArray(query.scopes), | |||
severities: serializeStringArray(query.severities), | |||
sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined, | |||
sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity), |
@@ -18,7 +18,8 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { colors } from '../app/theme'; | |||
import { IssueType } from '../types/issues'; | |||
import { ComponentQualifier } from '../types/component'; | |||
import { IssueScope, IssueType } from '../types/issues'; | |||
export const SEVERITIES = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; | |||
export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED']; | |||
@@ -28,6 +29,10 @@ export const ISSUE_TYPES: T.IssueType[] = [ | |||
IssueType.CodeSmell, | |||
IssueType.SecurityHotspot | |||
]; | |||
export const SOURCE_SCOPES = [ | |||
{ scope: IssueScope.Main, qualifier: ComponentQualifier.File }, | |||
{ scope: IssueScope.Test, qualifier: ComponentQualifier.TestFile } | |||
]; | |||
export const RULE_TYPES: T.RuleType[] = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT']; | |||
export const RULE_STATUSES = ['READY', 'BETA', 'DEPRECATED']; | |||
@@ -24,3 +24,8 @@ export enum IssueType { | |||
Bug = 'BUG', | |||
SecurityHotspot = 'SECURITY_HOTSPOT' | |||
} | |||
export enum IssueScope { | |||
Main = 'MAIN', | |||
Test = 'TEST' | |||
} |
@@ -785,6 +785,9 @@ issue.status.TO_REVIEW=To Review | |||
issue.status.IN_REVIEW=In Review | |||
issue.status.REVIEWED=Reviewed | |||
issue.scope.MAIN=Main code | |||
issue.scope.TEST=Test code | |||
issue.resolution.FALSE-POSITIVE=False Positive | |||
issue.resolution.FALSE-POSITIVE.description=Issues that manual review determined were False Positives. Effort from these issues is ignored. | |||
issue.resolution.FIXED=Fixed | |||
@@ -849,6 +852,7 @@ issue.changelog.field.file=File | |||
#------------------------------------------------------------------------------ | |||
issues.facet.types=Type | |||
issues.facet.severities=Severity | |||
issues.facet.scopes=Scope | |||
issues.facet.projects=Project | |||
issues.facet.statuses=Status | |||
issues.facet.hotspotStatuses=Hotspot Status |