@@ -38,7 +38,7 @@ interface Props { | |||
stats: T.Dict<number> | undefined; | |||
} | |||
const RESOLUTIONS = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED']; | |||
const RESOLUTIONS = ['', 'FALSE-POSITIVE', 'FIXED', 'REMOVED', 'WONTFIX']; | |||
export default class ResolutionFacet extends React.PureComponent<Props> { | |||
property = 'resolutions'; |
@@ -38,14 +38,14 @@ interface Props { | |||
statuses: string[]; | |||
} | |||
const STATUSES = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED']; | |||
const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED']; | |||
const HOTSPOT_STATUSES = ['TO_REVIEW', 'REVIEWED', 'IN_REVIEW']; | |||
const COMMON_STATUSES = ['CLOSED']; | |||
export default class StatusFacet extends React.PureComponent<Props> { | |||
property = 'statuses'; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
static defaultProps = { open: true }; | |||
handleItemClick = (itemValue: string, multiple: boolean) => { | |||
const { statuses } = this.props; | |||
@@ -110,7 +110,15 @@ export default class StatusFacet extends React.PureComponent<Props> { | |||
<DeferredSpinner loading={this.props.fetching} /> | |||
{this.props.open && ( | |||
<> | |||
<FacetItemsList>{STATUSES.map(this.renderItem)}</FacetItemsList> | |||
<FacetItemsList title={translate('issues')}> | |||
{STATUSES.map(this.renderItem)} | |||
</FacetItemsList> | |||
<FacetItemsList title={translate('issue.type.SECURITY_HOTSPOT.plural')}> | |||
{HOTSPOT_STATUSES.map(this.renderItem)} | |||
</FacetItemsList> | |||
<FacetItemsList title={translate('issues.issues_and_hotspots')}> | |||
{COMMON_STATUSES.map(this.renderItem)} | |||
</FacetItemsList> | |||
<MultipleSelectionHint options={Object.keys(stats).length} values={statuses.length} /> | |||
</> | |||
)} |
@@ -0,0 +1,81 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 StatusFacet from '../StatusFacet'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should toggle status facet', () => { | |||
const onToggle = jest.fn(); | |||
const wrapper = shallowRender({ onToggle }); | |||
click(wrapper.children('FacetHeader')); | |||
expect(onToggle).toBeCalledWith('statuses'); | |||
}); | |||
it('should clear status facet', () => { | |||
const onChange = jest.fn(); | |||
const wrapper = shallowRender({ onChange, statuses: ['TO_REVIEW'] }); | |||
wrapper.children('FacetHeader').prop<Function>('onClear')(); | |||
expect(onChange).toBeCalledWith({ statuses: [] }); | |||
}); | |||
it('should select a status', () => { | |||
const onChange = jest.fn(); | |||
const wrapper = shallowRender({ onChange }); | |||
clickAndCheck('TO_REVIEW'); | |||
clickAndCheck('OPEN', true, ['OPEN', 'TO_REVIEW']); | |||
clickAndCheck('CONFIRMED'); | |||
function clickAndCheck(status: string, multiple = false, expected = [status]) { | |||
wrapper | |||
.find(`FacetItemsList`) | |||
.find(`FacetItem[value="${status}"]`) | |||
.prop<Function>('onClick')(status, multiple); | |||
expect(onChange).lastCalledWith({ statuses: expected }); | |||
wrapper.setProps({ statuses: expected }); | |||
} | |||
}); | |||
function shallowRender(props: Partial<StatusFacet['props']> = {}) { | |||
return shallow( | |||
<StatusFacet | |||
fetching={false} | |||
onChange={jest.fn()} | |||
onToggle={jest.fn()} | |||
open={true} | |||
stats={{ | |||
OPEN: 104, | |||
CONFIRMED: 8, | |||
REOPENED: 0, | |||
RESOLVED: 0, | |||
CLOSED: 8, | |||
TO_REVIEW: 150, | |||
IN_REVIEW: 7, | |||
REVIEWED: 1105 | |||
}} | |||
statuses={[]} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,163 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<FacetBox | |||
property="statuses" | |||
> | |||
<FacetHeader | |||
name="issues.facet.statuses" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={Array []} | |||
/> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<FacetItemsList | |||
title="issues" | |||
> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={true} | |||
key="OPEN" | |||
loading={false} | |||
name={ | |||
<StatusHelper | |||
status="OPEN" | |||
/> | |||
} | |||
onClick={[Function]} | |||
stat="104" | |||
tooltip="issue.status.OPEN" | |||
value="OPEN" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={true} | |||
key="CONFIRMED" | |||
loading={false} | |||
name={ | |||
<StatusHelper | |||
status="CONFIRMED" | |||
/> | |||
} | |||
onClick={[Function]} | |||
stat="8" | |||
tooltip="issue.status.CONFIRMED" | |||
value="CONFIRMED" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={true} | |||
halfWidth={true} | |||
key="REOPENED" | |||
loading={false} | |||
name={ | |||
<StatusHelper | |||
status="REOPENED" | |||
/> | |||
} | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="issue.status.REOPENED" | |||
value="REOPENED" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={true} | |||
halfWidth={true} | |||
key="RESOLVED" | |||
loading={false} | |||
name={ | |||
<StatusHelper | |||
status="RESOLVED" | |||
/> | |||
} | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="issue.status.RESOLVED" | |||
value="RESOLVED" | |||
/> | |||
</FacetItemsList> | |||
<FacetItemsList | |||
title="issue.type.SECURITY_HOTSPOT.plural" | |||
> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={true} | |||
key="TO_REVIEW" | |||
loading={false} | |||
name={ | |||
<StatusHelper | |||
status="TO_REVIEW" | |||
/> | |||
} | |||
onClick={[Function]} | |||
stat="150" | |||
tooltip="issue.status.TO_REVIEW" | |||
value="TO_REVIEW" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={true} | |||
key="REVIEWED" | |||
loading={false} | |||
name={ | |||
<StatusHelper | |||
status="REVIEWED" | |||
/> | |||
} | |||
onClick={[Function]} | |||
stat="1.1short_number_suffix.k" | |||
tooltip="issue.status.REVIEWED" | |||
value="REVIEWED" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={true} | |||
key="IN_REVIEW" | |||
loading={false} | |||
name={ | |||
<StatusHelper | |||
status="IN_REVIEW" | |||
/> | |||
} | |||
onClick={[Function]} | |||
stat="7" | |||
tooltip="issue.status.IN_REVIEW" | |||
value="IN_REVIEW" | |||
/> | |||
</FacetItemsList> | |||
<FacetItemsList | |||
title="issues.issues_and_hotspots" | |||
> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={true} | |||
key="CLOSED" | |||
loading={false} | |||
name={ | |||
<StatusHelper | |||
status="CLOSED" | |||
/> | |||
} | |||
onClick={[Function]} | |||
stat="8" | |||
tooltip="issue.status.CLOSED" | |||
value="CLOSED" | |||
/> | |||
</FacetItemsList> | |||
<MultipleSelectionHint | |||
options={8} | |||
values={0} | |||
/> | |||
</FacetBox> | |||
`; |
@@ -21,8 +21,14 @@ import * as React from 'react'; | |||
interface Props { | |||
children?: React.ReactNode; | |||
title?: string; | |||
} | |||
export default function FacetItemsList(props: Props) { | |||
return <div className="search-navigator-facet-list">{props.children}</div>; | |||
export default function FacetItemsList({ children, title }: Props) { | |||
return ( | |||
<div className="search-navigator-facet-list"> | |||
{title && <div className="search-navigator-facet-list-title">{title}</div>} | |||
{children} | |||
</div> | |||
); | |||
} |
@@ -30,3 +30,13 @@ it('should render', () => { | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render with title', () => { | |||
expect( | |||
shallow( | |||
<FacetItemsList title="title test"> | |||
<div /> | |||
</FacetItemsList> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -7,3 +7,16 @@ exports[`should render 1`] = ` | |||
<div /> | |||
</div> | |||
`; | |||
exports[`should render with title 1`] = ` | |||
<div | |||
className="search-navigator-facet-list" | |||
> | |||
<div | |||
className="search-navigator-facet-list-title" | |||
> | |||
title test | |||
</div> | |||
<div /> | |||
</div> | |||
`; |
@@ -31,7 +31,10 @@ const statusIcons: T.Dict<(props: IconProps) => React.ReactElement<any>> = { | |||
confirmed: ConfirmedStatusIcon, | |||
reopened: ReopenedStatusIcon, | |||
resolved: ResolvedStatusIcon, | |||
closed: ClosedStatusIcon | |||
closed: ClosedStatusIcon, | |||
to_review: OpenStatusIcon, | |||
in_review: ConfirmedStatusIcon, | |||
reviewed: ResolvedStatusIcon | |||
}; | |||
export default function StatusIcon(props: Props) { |
@@ -358,6 +358,17 @@ a.search-navigator-facet:focus, | |||
font-size: 0; | |||
} | |||
.search-navigator-facet-list-title { | |||
margin: 0 var(--gridSize) calc(var(--gridSize) / 2); | |||
font-size: var(--smallFontSize); | |||
font-weight: bold; | |||
} | |||
.search-navigator-facet-list + .search-navigator-facet-list > .search-navigator-facet-list-title { | |||
border-top: 1px solid var(--barBorderColor); | |||
padding-top: var(--gridSize); | |||
} | |||
.search-navigator-facet-empty { | |||
margin: 0 0 0 0; | |||
padding: 0 10px 10px; |
@@ -645,23 +645,19 @@ issue.type.VULNERABILITY.plural=Vulnerabilities | |||
issue.type.SECURITY_HOTSPOT.plural=Security Hotspots | |||
issue.status.REOPENED=Reopened | |||
issue.status.REOPENED.description=Transitioned to and then back from some other status. | |||
issue.status.RESOLVED=Resolved | |||
issue.status.RESOLVED.description=Manually marked as corrected. | |||
issue.status.OPEN=Open | |||
issue.status.OPEN.description=Untouched. This status is set automatically at issue creation. | |||
issue.status.CONFIRMED=Confirmed | |||
issue.status.CONFIRMED.description=Manually examined and affirmed as an issue that needs attention. | |||
issue.status.CLOSED=Closed | |||
issue.status.CLOSED.description=Non-active and no longer requiring attention. | |||
issue.status.TOREVIEW=To Review | |||
issue.status.TOREVIEW.description=A review is required to check for a vulnerability. | |||
issue.status.TO_REVIEW=To Review | |||
issue.status.IN_REVIEW=In Review | |||
issue.status.REVIEWED=Reviewed | |||
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 | |||
issue.resolution.FIXED.description=Issues that were corrected in code and reanalyzed. | |||
issue.resolution.WONTFIX=Won't fix | |||
issue.resolution.WONTFIX=Won't Fix | |||
issue.resolution.WONTFIX.description=Issues that are accepted in this context. They and their effort will be ignored. | |||
issue.resolution.REMOVED=Removed | |||
issue.resolution.REMOVED.description=Either the rule or the resource was changed (removed, relocated, parameters changed, etc.) so that analysis no longer finds these issues. | |||
@@ -687,6 +683,7 @@ issues.my_issues=My Issues | |||
issues.no_my_issues=There are no issues assigned to you. | |||
issues.no_issues=No Issues. Hooray! | |||
issues.x_more_locations=+ {0} more location(s) | |||
issues.issues_and_hotspots=Issues & Security Hotspots | |||
issues.hotspots.helper=Security Hotspots aren't necessarily issues, but they need to be reviewed to make sure they aren't vulnerabilities. | |||
@@ -723,6 +720,7 @@ issues.facet.types=Type | |||
issues.facet.severities=Severity | |||
issues.facet.projects=Project | |||
issues.facet.statuses=Status | |||
issues.facet.hotspotStatuses=Hotspot Status | |||
issues.facet.assignees=Assignee | |||
issues.facet.files=File | |||
issues.facet.modules=Module | |||
@@ -2100,7 +2098,7 @@ projects_role.admin.desc=Access project settings and perform administration task | |||
projects_role.issueadmin=Administer Issues | |||
projects_role.issueadmin.desc=Change the type and severity of issues, resolve issues as being "won't fix" or "false-positive" (users also need "Browse" permission). | |||
projects_role.securityhotspotadmin=Administer Security Hotspots | |||
projects_role.securityhotspotadmin.desc=Detect a Vulnerability from a Security Hotspot. Reject, clear, accept, reopen a Security Hotspot (users also need "Browse" permissions). | |||
projects_role.securityhotspotadmin.desc=Open a Vulnerability from a Security Hotspot. Resolved a Security Hotspot as reviewed, set it as in review or reset it as to review (users also need Browse permission). | |||
projects_role.user=Browse | |||
projects_role.user.desc=Access a project, browse its measures and issues, confirm or resolve issues as "fixed", change the assignee, comment on issues and change tags. | |||
projects_role.codeviewer=See Source Code |