@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Location } from 'history'; | |||
import { flatMap, range } from 'lodash'; | |||
import * as React from 'react'; | |||
import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages'; | |||
import { getMeasures } from '../../api/measures'; | |||
@@ -35,7 +36,6 @@ import { | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusFilter, | |||
HotspotUpdate, | |||
RawHotspot | |||
} from '../../types/security-hotspots'; | |||
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer'; | |||
@@ -61,7 +61,7 @@ interface State { | |||
loadingMeasure: boolean; | |||
loadingMore: boolean; | |||
securityCategories: T.StandardSecurityCategories; | |||
selectedHotspotKey: string | undefined; | |||
selectedHotspot: RawHotspot | undefined; | |||
filters: HotspotFilters; | |||
} | |||
@@ -79,7 +79,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
hotspots: [], | |||
hotspotsPageIndex: 1, | |||
securityCategories: {}, | |||
selectedHotspotKey: undefined, | |||
selectedHotspot: undefined, | |||
filters: { | |||
...this.constructFiltersFromProps(props), | |||
status: HotspotStatusFilter.TO_REVIEW | |||
@@ -150,13 +150,13 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
hotspotsTotal: paging.total, | |||
loading: false, | |||
securityCategories: sonarsourceSecurity, | |||
selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined | |||
selectedHotspot: hotspots.length > 0 ? hotspots[0] : undefined | |||
}); | |||
}) | |||
.catch(this.handleCallFailure); | |||
} | |||
fetchSecurityHotspotsReviewed() { | |||
fetchSecurityHotspotsReviewed = () => { | |||
const { branchLike, component } = this.props; | |||
const { filters } = this.state; | |||
@@ -186,7 +186,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
this.setState({ loadingMeasure: false }); | |||
} | |||
}); | |||
} | |||
}; | |||
fetchSecurityHotspots(page = 1) { | |||
const { branchLike, component, location } = this.props; | |||
@@ -241,7 +241,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
hotspotsPageIndex: 1, | |||
hotspotsTotal: paging.total, | |||
loading: false, | |||
selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined | |||
selectedHotspot: hotspots.length > 0 ? hotspots[0] : undefined | |||
}); | |||
}) | |||
.catch(this.handleCallFailure); | |||
@@ -259,24 +259,30 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
); | |||
}; | |||
handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key }); | |||
handleHotspotClick = (selectedHotspot: RawHotspot) => this.setState({ selectedHotspot }); | |||
handleHotspotUpdate = ({ key, status, resolution }: HotspotUpdate) => { | |||
this.setState(({ hotspots }) => { | |||
const index = hotspots.findIndex(h => h.key === key); | |||
handleHotspotUpdate = (hotspotKey: string) => { | |||
const { hotspots, hotspotsPageIndex } = this.state; | |||
const index = hotspots.findIndex(h => h.key === hotspotKey); | |||
if (index > -1) { | |||
const hotspot = { | |||
...hotspots[index], | |||
status, | |||
resolution | |||
}; | |||
return Promise.all( | |||
range(hotspotsPageIndex).map(p => this.fetchSecurityHotspots(p + 1 /* pages are 1-indexed */)) | |||
) | |||
.then(hotspotPages => { | |||
const allHotspots = flatMap(hotspotPages, 'hotspots'); | |||
return { hotspots: [...hotspots.slice(0, index), hotspot, ...hotspots.slice(index + 1)] }; | |||
} | |||
return null; | |||
}); | |||
return this.fetchSecurityHotspotsReviewed(); | |||
const { paging } = hotspotPages[hotspotPages.length - 1]; | |||
const nextHotspot = allHotspots[Math.min(index, allHotspots.length - 1)]; | |||
this.setState({ | |||
hotspots: allHotspots, | |||
hotspotsPageIndex: paging.pageIndex, | |||
hotspotsTotal: paging.total, | |||
selectedHotspot: nextHotspot | |||
}); | |||
}) | |||
.then(this.fetchSecurityHotspotsReviewed); | |||
}; | |||
handleShowAllHotspots = () => { | |||
@@ -317,7 +323,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
loadingMeasure, | |||
loadingMore, | |||
securityCategories, | |||
selectedHotspotKey, | |||
selectedHotspot, | |||
filters | |||
} = this.state; | |||
@@ -339,7 +345,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
onShowAllHotspots={this.handleShowAllHotspots} | |||
onUpdateHotspot={this.handleHotspotUpdate} | |||
securityCategories={securityCategories} | |||
selectedHotspotKey={selectedHotspotKey} | |||
selectedHotspot={selectedHotspot} | |||
/> | |||
); | |||
} |
@@ -26,12 +26,7 @@ import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; | |||
import ScreenPositionHelper from '../../components/common/ScreenPositionHelper'; | |||
import { isBranch } from '../../helpers/branch-like'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { | |||
HotspotFilters, | |||
HotspotStatusFilter, | |||
HotspotUpdate, | |||
RawHotspot | |||
} from '../../types/security-hotspots'; | |||
import { HotspotFilters, HotspotStatusFilter, RawHotspot } from '../../types/security-hotspots'; | |||
import EmptyHotspotsPage from './components/EmptyHotspotsPage'; | |||
import FilterBar from './components/FilterBar'; | |||
import HotspotList from './components/HotspotList'; | |||
@@ -50,11 +45,11 @@ export interface SecurityHotspotsAppRendererProps { | |||
loadingMeasure: boolean; | |||
loadingMore: boolean; | |||
onChangeFilters: (filters: Partial<HotspotFilters>) => void; | |||
onHotspotClick: (key: string) => void; | |||
onHotspotClick: (hotspot: RawHotspot) => void; | |||
onLoadMore: () => void; | |||
onShowAllHotspots: () => void; | |||
onUpdateHotspot: (hotspot: HotspotUpdate) => void; | |||
selectedHotspotKey?: string; | |||
onUpdateHotspot: (hotspotKey: string) => Promise<void>; | |||
selectedHotspot: RawHotspot | undefined; | |||
securityCategories: T.StandardSecurityCategories; | |||
} | |||
@@ -70,7 +65,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe | |||
loadingMeasure, | |||
loadingMore, | |||
securityCategories, | |||
selectedHotspotKey, | |||
selectedHotspot, | |||
filters | |||
} = props; | |||
@@ -98,7 +93,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe | |||
<DeferredSpinner className="huge-spacer-left big-spacer-top" /> | |||
) : ( | |||
<> | |||
{hotspots.length === 0 ? ( | |||
{hotspots.length === 0 || !selectedHotspot ? ( | |||
<EmptyHotspotsPage | |||
filtered={ | |||
filters.assignedToMe || | |||
@@ -118,19 +113,17 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe | |||
onHotspotClick={props.onHotspotClick} | |||
onLoadMore={props.onLoadMore} | |||
securityCategories={securityCategories} | |||
selectedHotspotKey={selectedHotspotKey} | |||
selectedHotspot={selectedHotspot} | |||
statusFilter={filters.status} | |||
/> | |||
</div> | |||
<div className="main"> | |||
{selectedHotspotKey && ( | |||
<HotspotViewer | |||
branchLike={branchLike} | |||
hotspotKey={selectedHotspotKey} | |||
onUpdateHotspot={props.onUpdateHotspot} | |||
securityCategories={securityCategories} | |||
/> | |||
)} | |||
<HotspotViewer | |||
branchLike={branchLike} | |||
hotspotKey={selectedHotspot.key} | |||
onUpdateHotspot={props.onUpdateHotspot} | |||
securityCategories={securityCategories} | |||
/> | |||
</div> | |||
</div> | |||
)} |
@@ -99,7 +99,7 @@ it('should load data correctly', async () => { | |||
expect(wrapper.state().loading).toBe(false); | |||
expect(wrapper.state().hotspots).toEqual(hotspots); | |||
expect(wrapper.state().selectedHotspotKey).toBe(hotspots[0].key); | |||
expect(wrapper.state().selectedHotspot).toBe(hotspots[0]); | |||
expect(wrapper.state().securityCategories).toEqual({ | |||
cat1: { title: 'cat 1' } | |||
}); | |||
@@ -219,35 +219,43 @@ it('should handle hotspot update', async () => { | |||
const hotspots = [mockRawHotspot(), mockRawHotspot({ key })]; | |||
(getSecurityHotspots as jest.Mock).mockResolvedValueOnce({ | |||
hotspots, | |||
paging: { total: 2 } | |||
paging: { pageIndex: 1, total: 1252 } | |||
}); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ hotspotsPageIndex: 2 }); | |||
wrapper | |||
jest.clearAllMocks(); | |||
(getSecurityHotspots as jest.Mock) | |||
.mockResolvedValueOnce({ | |||
hotspots: [mockRawHotspot()], | |||
paging: { pageIndex: 1, total: 1251 } | |||
}) | |||
.mockResolvedValueOnce({ | |||
hotspots: [mockRawHotspot()], | |||
paging: { pageIndex: 2, total: 1251 } | |||
}); | |||
const selectedHotspotIndex = wrapper | |||
.state() | |||
.hotspots.findIndex(h => h.key === wrapper.state().selectedHotspot?.key); | |||
await wrapper | |||
.find(SecurityHotspotsAppRenderer) | |||
.props() | |||
.onUpdateHotspot({ key, status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE }); | |||
.onUpdateHotspot(key); | |||
expect(wrapper.state().hotspots[0]).toEqual(hotspots[0]); | |||
expect(wrapper.state().hotspots[1]).toEqual({ | |||
...hotspots[1], | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.SAFE | |||
}); | |||
expect(getMeasures).toBeCalled(); | |||
expect(getSecurityHotspots).toHaveBeenCalledTimes(2); | |||
await waitAndUpdate(wrapper); | |||
const previousState = wrapper.state(); | |||
wrapper.instance().handleHotspotUpdate({ | |||
key: 'unknown', | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.SAFE | |||
}); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state()).toEqual(previousState); | |||
expect(wrapper.state().hotspots).toHaveLength(2); | |||
expect(wrapper.state().hotspotsPageIndex).toBe(2); | |||
expect(wrapper.state().hotspotsTotal).toBe(1251); | |||
expect( | |||
wrapper.state().hotspots.findIndex(h => h.key === wrapper.state().selectedHotspot?.key) | |||
).toBe(selectedHotspotIndex); | |||
expect(getMeasures).toBeCalled(); | |||
}); | |||
it('should handle status filter change', async () => { |
@@ -51,7 +51,7 @@ it('should render correctly with hotspots', () => { | |||
.dive() | |||
).toMatchSnapshot(); | |||
expect( | |||
shallowRender({ hotspots, hotspotsTotal: 3, selectedHotspotKey: 'h2' }) | |||
shallowRender({ hotspots, hotspotsTotal: 3, selectedHotspot: mockRawHotspot({ key: 'h2' }) }) | |||
.find(ScreenPositionHelper) | |||
.dive() | |||
).toMatchSnapshot(); | |||
@@ -89,6 +89,7 @@ function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) { | |||
onShowAllHotspots={jest.fn()} | |||
onUpdateHotspot={jest.fn()} | |||
securityCategories={{}} | |||
selectedHotspot={undefined} | |||
{...props} | |||
/> | |||
); |
@@ -46,60 +46,10 @@ exports[`should render correctly with hotspots 1`] = ` | |||
<A11ySkipTarget | |||
anchor="security_hotspots_main" | |||
/> | |||
<div | |||
className="layout-page" | |||
> | |||
<div | |||
className="sidebar" | |||
> | |||
<HotspotList | |||
hotspots={ | |||
Array [ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h1", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
}, | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
}, | |||
] | |||
} | |||
hotspotsTotal={2} | |||
isStaticListOfHotspots={true} | |||
loadingMore={false} | |||
onHotspotClick={[MockFunction]} | |||
onLoadMore={[MockFunction]} | |||
securityCategories={Object {}} | |||
statusFilter="TO_REVIEW" | |||
/> | |||
</div> | |||
<div | |||
className="main" | |||
/> | |||
</div> | |||
<EmptyHotspotsPage | |||
filtered={false} | |||
isStaticListOfHotspots={true} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
@@ -172,7 +122,23 @@ exports[`should render correctly with hotspots 2`] = ` | |||
onHotspotClick={[MockFunction]} | |||
onLoadMore={[MockFunction]} | |||
securityCategories={Object {}} | |||
selectedHotspotKey="h2" | |||
selectedHotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
statusFilter="TO_REVIEW" | |||
/> | |||
</div> |
@@ -125,19 +125,10 @@ describe('sortHotspots', () => { | |||
}); | |||
describe('groupByCategory', () => { | |||
it('should group and sort properly', () => { | |||
it('should group properly', () => { | |||
const result = groupByCategory(hotspots, categories); | |||
expect(result).toHaveLength(7); | |||
expect(result.map(g => g.key)).toEqual([ | |||
'xss', | |||
'dos', | |||
'log-injection', | |||
'object-injection', | |||
'ssrf', | |||
'xxe', | |||
'xpath-injection' | |||
]); | |||
}); | |||
}); | |||
@@ -25,17 +25,17 @@ import { RawHotspot } from '../../../types/security-hotspots'; | |||
import HotspotListItem from './HotspotListItem'; | |||
export interface HotspotCategoryProps { | |||
categoryKey: string; | |||
expanded: boolean; | |||
hotspots: RawHotspot[]; | |||
onHotspotClick: (key: string) => void; | |||
selectedHotspotKey: string | undefined; | |||
startsExpanded: boolean; | |||
onHotspotClick: (hotspot: RawHotspot) => void; | |||
onToggleExpand: (categoryKey: string, value: boolean) => void; | |||
selectedHotspot: RawHotspot; | |||
title: string; | |||
} | |||
export default function HotspotCategory(props: HotspotCategoryProps) { | |||
const { hotspots, selectedHotspotKey, startsExpanded, title } = props; | |||
const [expanded, setExpanded] = React.useState(startsExpanded); | |||
const { categoryKey, expanded, hotspots, selectedHotspot, title } = props; | |||
if (hotspots.length < 1) { | |||
return null; | |||
@@ -46,9 +46,12 @@ export default function HotspotCategory(props: HotspotCategoryProps) { | |||
return ( | |||
<div className={classNames('hotspot-category', risk)}> | |||
<a | |||
className="hotspot-category-header display-flex-space-between display-flex-center" | |||
className={classNames( | |||
'hotspot-category-header display-flex-space-between display-flex-center', | |||
{ 'contains-selected-hotspot': selectedHotspot.securityCategory === categoryKey } | |||
)} | |||
href="#" | |||
onClick={() => setExpanded(!expanded)}> | |||
onClick={() => props.onToggleExpand(categoryKey, !expanded)}> | |||
<strong className="flex-1">{title}</strong> | |||
<span> | |||
<span className="counter-badge">{hotspots.length}</span> | |||
@@ -66,7 +69,7 @@ export default function HotspotCategory(props: HotspotCategoryProps) { | |||
<HotspotListItem | |||
hotspot={h} | |||
onClick={props.onHotspotClick} | |||
selected={h.key === selectedHotspotKey} | |||
selected={h.key === selectedHotspot.key} | |||
/> | |||
</li> | |||
))} |
@@ -37,7 +37,8 @@ | |||
border-left: 4px solid; | |||
} | |||
.hotspot-category .hotspot-category-header:hover { | |||
.hotspot-category .hotspot-category-header:hover, | |||
.hotspot-category .hotspot-category-header.contains-selected-hotspot { | |||
color: var(--blue); | |||
} | |||
@@ -28,81 +28,127 @@ import { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils'; | |||
import HotspotCategory from './HotspotCategory'; | |||
import './HotspotList.css'; | |||
export interface HotspotListProps { | |||
interface Props { | |||
hotspots: RawHotspot[]; | |||
hotspotsTotal?: number; | |||
isStaticListOfHotspots: boolean; | |||
loadingMore: boolean; | |||
onHotspotClick: (key: string) => void; | |||
onHotspotClick: (hotspot: RawHotspot) => void; | |||
onLoadMore: () => void; | |||
securityCategories: T.StandardSecurityCategories; | |||
selectedHotspotKey: string | undefined; | |||
selectedHotspot: RawHotspot; | |||
statusFilter: HotspotStatusFilter; | |||
} | |||
export default function HotspotList(props: HotspotListProps) { | |||
const { | |||
hotspots, | |||
hotspotsTotal, | |||
isStaticListOfHotspots, | |||
loadingMore, | |||
securityCategories, | |||
selectedHotspotKey, | |||
statusFilter | |||
} = props; | |||
const groupedHotspots: Array<{ | |||
interface State { | |||
expandedCategories: T.Dict<boolean>; | |||
groupedHotspots: Array<{ | |||
risk: RiskExposure; | |||
categories: Array<{ key: string; hotspots: RawHotspot[]; title: string }>; | |||
}> = React.useMemo(() => { | |||
}>; | |||
} | |||
export default class HotspotList extends React.Component<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
expandedCategories: { [props.selectedHotspot.securityCategory]: true }, | |||
groupedHotspots: this.groupHotspots(props.hotspots, props.securityCategories) | |||
}; | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
// Force open the category of selected hotspot | |||
if ( | |||
this.props.selectedHotspot.securityCategory !== prevProps.selectedHotspot.securityCategory | |||
) { | |||
this.handleToggleCategory(this.props.selectedHotspot.securityCategory, true); | |||
} | |||
// Compute the hotspot tree from the list | |||
if ( | |||
this.props.hotspots !== prevProps.hotspots || | |||
this.props.securityCategories !== prevProps.securityCategories | |||
) { | |||
const groupedHotspots = this.groupHotspots( | |||
this.props.hotspots, | |||
this.props.securityCategories | |||
); | |||
this.setState({ groupedHotspots }); | |||
} | |||
} | |||
groupHotspots = (hotspots: RawHotspot[], securityCategories: T.StandardSecurityCategories) => { | |||
const risks = groupBy(hotspots, h => h.vulnerabilityProbability); | |||
return RISK_EXPOSURE_LEVELS.map(risk => ({ | |||
risk, | |||
categories: groupByCategory(risks[risk], securityCategories) | |||
})).filter(risk => risk.categories.length > 0); | |||
}, [hotspots, securityCategories]); | |||
}; | |||
handleToggleCategory = (categoryKey: string, value: boolean) => { | |||
this.setState(({ expandedCategories }) => ({ | |||
expandedCategories: { ...expandedCategories, [categoryKey]: value } | |||
})); | |||
}; | |||
render() { | |||
const { | |||
hotspots, | |||
hotspotsTotal, | |||
isStaticListOfHotspots, | |||
loadingMore, | |||
selectedHotspot, | |||
statusFilter | |||
} = this.props; | |||
const { expandedCategories, groupedHotspots } = this.state; | |||
return ( | |||
<div className="huge-spacer-bottom"> | |||
<h1 className="hotspot-list-header bordered-bottom"> | |||
<SecurityHotspotIcon className="spacer-right" /> | |||
{translateWithParameters( | |||
isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`, | |||
hotspots.length | |||
)} | |||
</h1> | |||
<ul className="big-spacer-bottom"> | |||
{groupedHotspots.map((riskGroup, groupIndex) => ( | |||
<li className="big-spacer-bottom" key={riskGroup.risk}> | |||
<div className="hotspot-risk-header little-spacer-left"> | |||
<span>{translate('hotspots.risk_exposure')}</span> | |||
<div className={classNames('hotspot-risk-badge', 'spacer-left', riskGroup.risk)}> | |||
{translate('risk_exposure', riskGroup.risk)} | |||
return ( | |||
<div className="huge-spacer-bottom"> | |||
<h1 className="hotspot-list-header bordered-bottom"> | |||
<SecurityHotspotIcon className="spacer-right" /> | |||
{translateWithParameters( | |||
isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`, | |||
hotspots.length | |||
)} | |||
</h1> | |||
<ul className="big-spacer-bottom"> | |||
{groupedHotspots.map(riskGroup => ( | |||
<li className="big-spacer-bottom" key={riskGroup.risk}> | |||
<div className="hotspot-risk-header little-spacer-left"> | |||
<span>{translate('hotspots.risk_exposure')}</span> | |||
<div className={classNames('hotspot-risk-badge', 'spacer-left', riskGroup.risk)}> | |||
{translate('risk_exposure', riskGroup.risk)} | |||
</div> | |||
</div> | |||
</div> | |||
<ul> | |||
{riskGroup.categories.map((cat, catIndex) => ( | |||
<li className="spacer-bottom" key={cat.key}> | |||
<HotspotCategory | |||
hotspots={cat.hotspots} | |||
onHotspotClick={props.onHotspotClick} | |||
selectedHotspotKey={selectedHotspotKey} | |||
startsExpanded={groupIndex === 0 && catIndex === 0} | |||
title={cat.title} | |||
/> | |||
</li> | |||
))} | |||
</ul> | |||
</li> | |||
))} | |||
</ul> | |||
<ListFooter | |||
count={hotspots.length} | |||
loadMore={!loadingMore ? props.onLoadMore : undefined} | |||
loading={loadingMore} | |||
total={hotspotsTotal} | |||
/> | |||
</div> | |||
); | |||
<ul> | |||
{riskGroup.categories.map(cat => ( | |||
<li className="spacer-bottom" key={cat.key}> | |||
<HotspotCategory | |||
categoryKey={cat.key} | |||
expanded={expandedCategories[cat.key]} | |||
hotspots={cat.hotspots} | |||
onHotspotClick={this.props.onHotspotClick} | |||
onToggleExpand={this.handleToggleCategory} | |||
selectedHotspot={selectedHotspot} | |||
title={cat.title} | |||
/> | |||
</li> | |||
))} | |||
</ul> | |||
</li> | |||
))} | |||
</ul> | |||
<ListFooter | |||
count={hotspots.length} | |||
loadMore={!loadingMore ? this.props.onLoadMore : undefined} | |||
loading={loadingMore} | |||
total={hotspotsTotal} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -25,7 +25,7 @@ import { getStatusOptionFromStatusAndResolution } from '../utils'; | |||
export interface HotspotListItemProps { | |||
hotspot: RawHotspot; | |||
onClick: (key: string) => void; | |||
onClick: (hotspot: RawHotspot) => void; | |||
selected: boolean; | |||
} | |||
@@ -35,7 +35,7 @@ export default function HotspotListItem(props: HotspotListItemProps) { | |||
<a | |||
className={classNames('hotspot-item', { highlight: selected })} | |||
href="#" | |||
onClick={() => !selected && props.onClick(hotspot.key)}> | |||
onClick={() => !selected && props.onClick(hotspot)}> | |||
<div className="little-spacer-left">{hotspot.message}</div> | |||
<div className="badge spacer-top"> | |||
{translate( |
@@ -21,13 +21,13 @@ | |||
import * as React from 'react'; | |||
import { getSecurityHotspotDetails } from '../../../api/security-hotspots'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { Hotspot, HotspotUpdate } from '../../../types/security-hotspots'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
import HotspotViewerRenderer from './HotspotViewerRenderer'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
hotspotKey: string; | |||
onUpdateHotspot: (hotspot: HotspotUpdate) => void; | |||
onUpdateHotspot: (hotspotKey: string) => Promise<void>; | |||
securityCategories: T.StandardSecurityCategories; | |||
} | |||
@@ -70,11 +70,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> { | |||
handleHotspotUpdate = () => { | |||
return this.fetchHotspot().then((hotspot?: Hotspot) => { | |||
if (hotspot) { | |||
this.props.onUpdateHotspot({ | |||
key: hotspot.key, | |||
status: hotspot.status, | |||
resolution: hotspot.resolution | |||
}); | |||
return this.props.onUpdateHotspot(hotspot.key); | |||
} | |||
}); | |||
}; |
@@ -31,7 +31,7 @@ export interface HotspotViewerRendererProps { | |||
branchLike?: BranchLike; | |||
hotspot?: Hotspot; | |||
loading: boolean; | |||
onUpdateHotspot: () => void; | |||
onUpdateHotspot: () => Promise<void>; | |||
securityCategories: T.StandardSecurityCategories; | |||
} | |||
@@ -27,30 +27,49 @@ it('should render correctly', () => { | |||
}); | |||
it('should render correctly with hotspots', () => { | |||
const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })]; | |||
const securityCategory = 'command-injection'; | |||
const hotspots = [ | |||
mockRawHotspot({ key: 'h1', securityCategory }), | |||
mockRawHotspot({ key: 'h2', securityCategory }) | |||
]; | |||
expect(shallowRender({ hotspots })).toMatchSnapshot(); | |||
expect(shallowRender({ hotspots, startsExpanded: false })).toMatchSnapshot('collapsed'); | |||
expect(shallowRender({ hotspots, expanded: false })).toMatchSnapshot('collapsed'); | |||
expect( | |||
shallowRender({ categoryKey: securityCategory, hotspots, selectedHotspot: hotspots[0] }) | |||
).toMatchSnapshot('contains selected'); | |||
}); | |||
it('should handle collapse and expand', () => { | |||
const wrapper = shallowRender({ hotspots: [mockRawHotspot()] }); | |||
const onToggleExpand = jest.fn(); | |||
const categoryKey = 'xss-injection'; | |||
const wrapper = shallowRender({ | |||
categoryKey, | |||
expanded: true, | |||
hotspots: [mockRawHotspot()], | |||
onToggleExpand | |||
}); | |||
wrapper.find('.hotspot-category-header').simulate('click'); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(onToggleExpand).toBeCalledWith(categoryKey, false); | |||
wrapper.setProps({ expanded: false }); | |||
wrapper.find('.hotspot-category-header').simulate('click'); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(onToggleExpand).toBeCalledWith(categoryKey, true); | |||
}); | |||
function shallowRender(props: Partial<HotspotCategoryProps> = {}) { | |||
return shallow( | |||
<HotspotCategory | |||
categoryKey="xss-injection" | |||
expanded={true} | |||
hotspots={[]} | |||
onHotspotClick={jest.fn()} | |||
selectedHotspotKey="" | |||
startsExpanded={true} | |||
onToggleExpand={jest.fn()} | |||
selectedHotspot={mockRawHotspot()} | |||
title="Class Injection" | |||
{...props} | |||
/> |
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { HotspotStatusFilter, RiskExposure } from '../../../../types/security-hotspots'; | |||
import HotspotList, { HotspotListProps } from '../HotspotList'; | |||
import HotspotList from '../HotspotList'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
@@ -32,32 +32,53 @@ it('should render correctly when the list of hotspot is static', () => { | |||
expect(shallowRender({ isStaticListOfHotspots: true })).toMatchSnapshot(); | |||
}); | |||
const hotspots = [ | |||
mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }), | |||
mockRawHotspot({ key: 'h2', securityCategory: 'cat1' }), | |||
mockRawHotspot({ | |||
key: 'h3', | |||
securityCategory: 'cat1', | |||
vulnerabilityProbability: RiskExposure.MEDIUM | |||
}), | |||
mockRawHotspot({ | |||
key: 'h4', | |||
securityCategory: 'cat1', | |||
vulnerabilityProbability: RiskExposure.MEDIUM | |||
}), | |||
mockRawHotspot({ | |||
key: 'h5', | |||
securityCategory: 'cat2', | |||
vulnerabilityProbability: RiskExposure.MEDIUM | |||
}) | |||
]; | |||
it('should render correctly with hotspots', () => { | |||
const hotspots = [ | |||
mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }), | |||
mockRawHotspot({ key: 'h2', securityCategory: 'cat1' }), | |||
mockRawHotspot({ | |||
key: 'h3', | |||
securityCategory: 'cat1', | |||
vulnerabilityProbability: RiskExposure.MEDIUM | |||
}), | |||
mockRawHotspot({ | |||
key: 'h4', | |||
securityCategory: 'cat1', | |||
vulnerabilityProbability: RiskExposure.MEDIUM | |||
}), | |||
mockRawHotspot({ | |||
key: 'h5', | |||
securityCategory: 'cat2', | |||
vulnerabilityProbability: RiskExposure.MEDIUM | |||
}) | |||
]; | |||
expect(shallowRender({ hotspots })).toMatchSnapshot('no pagination'); | |||
expect(shallowRender({ hotspots, hotspotsTotal: 7 })).toMatchSnapshot('pagination'); | |||
}); | |||
function shallowRender(props: Partial<HotspotListProps> = {}) { | |||
return shallow( | |||
it('should update expanded categories correctly', () => { | |||
const wrapper = shallowRender({ hotspots, selectedHotspot: hotspots[0] }); | |||
expect(wrapper.state().expandedCategories).toEqual({ cat2: true }); | |||
wrapper.setProps({ selectedHotspot: hotspots[1] }); | |||
expect(wrapper.state().expandedCategories).toEqual({ cat1: true, cat2: true }); | |||
}); | |||
it('should update grouped hotspots when the list changes', () => { | |||
const wrapper = shallowRender({ hotspots, selectedHotspot: hotspots[0] }); | |||
wrapper.setProps({ hotspots: [mockRawHotspot()] }); | |||
expect(wrapper.state().groupedHotspots).toHaveLength(1); | |||
expect(wrapper.state().groupedHotspots[0].categories).toHaveLength(1); | |||
expect(wrapper.state().groupedHotspots[0].categories[0].hotspots).toHaveLength(1); | |||
}); | |||
function shallowRender(props: Partial<HotspotList['props']> = {}) { | |||
return shallow<HotspotList>( | |||
<HotspotList | |||
hotspots={[]} | |||
isStaticListOfHotspots={false} | |||
@@ -65,7 +86,7 @@ function shallowRender(props: Partial<HotspotListProps> = {}) { | |||
onHotspotClick={jest.fn()} | |||
onLoadMore={jest.fn()} | |||
securityCategories={{}} | |||
selectedHotspotKey="h2" | |||
selectedHotspot={mockRawHotspot({ key: 'h2' })} | |||
statusFilter={HotspotStatusFilter.TO_REVIEW} | |||
{...props} | |||
/> |
@@ -34,7 +34,7 @@ it('should handle click', () => { | |||
wrapper.simulate('click'); | |||
expect(onClick).toBeCalledWith(hotspot.key); | |||
expect(onClick).toBeCalledWith(hotspot); | |||
}); | |||
function shallowRender(props: Partial<HotspotListItemProps> = {}) { |
@@ -1,34 +1,6 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should handle collapse and expand 1`] = ` | |||
<div | |||
className="hotspot-category HIGH" | |||
> | |||
<a | |||
className="hotspot-category-header display-flex-space-between display-flex-center" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<strong | |||
className="flex-1" | |||
> | |||
Class Injection | |||
</strong> | |||
<span> | |||
<span | |||
className="counter-badge" | |||
> | |||
1 | |||
</span> | |||
<ChevronDownIcon | |||
className="big-spacer-left" | |||
/> | |||
</span> | |||
</a> | |||
</div> | |||
`; | |||
exports[`should handle collapse and expand 2`] = ` | |||
exports[`should render correctly with hotspots 1`] = ` | |||
<div | |||
className="hotspot-category HIGH" | |||
> | |||
@@ -46,7 +18,7 @@ exports[`should handle collapse and expand 2`] = ` | |||
<span | |||
className="counter-badge" | |||
> | |||
1 | |||
2 | |||
</span> | |||
<ChevronUpIcon | |||
className="big-spacer-left" | |||
@@ -55,7 +27,32 @@ exports[`should handle collapse and expand 2`] = ` | |||
</a> | |||
<ul> | |||
<li | |||
key="01fc972e-2a3c-433e-bcae-0bd7f88f5123" | |||
key="h1" | |||
> | |||
<HotspotListItem | |||
hotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h1", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
onClick={[MockFunction]} | |||
selected={false} | |||
/> | |||
</li> | |||
<li | |||
key="h2" | |||
> | |||
<HotspotListItem | |||
hotspot={ | |||
@@ -63,7 +60,7 @@ exports[`should handle collapse and expand 2`] = ` | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
@@ -83,7 +80,7 @@ exports[`should handle collapse and expand 2`] = ` | |||
</div> | |||
`; | |||
exports[`should render correctly with hotspots 1`] = ` | |||
exports[`should render correctly with hotspots: collapsed 1`] = ` | |||
<div | |||
className="hotspot-category HIGH" | |||
> | |||
@@ -91,6 +88,34 @@ exports[`should render correctly with hotspots 1`] = ` | |||
className="hotspot-category-header display-flex-space-between display-flex-center" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<strong | |||
className="flex-1" | |||
> | |||
Class Injection | |||
</strong> | |||
<span> | |||
<span | |||
className="counter-badge" | |||
> | |||
2 | |||
</span> | |||
<ChevronDownIcon | |||
className="big-spacer-left" | |||
/> | |||
</span> | |||
</a> | |||
</div> | |||
`; | |||
exports[`should render correctly with hotspots: contains selected 1`] = ` | |||
<div | |||
className="hotspot-category HIGH" | |||
> | |||
<a | |||
className="hotspot-category-header display-flex-space-between display-flex-center contains-selected-hotspot" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<strong | |||
className="flex-1" | |||
@@ -131,7 +156,7 @@ exports[`should render correctly with hotspots 1`] = ` | |||
} | |||
} | |||
onClick={[MockFunction]} | |||
selected={false} | |||
selected={true} | |||
/> | |||
</li> | |||
<li | |||
@@ -163,32 +188,4 @@ exports[`should render correctly with hotspots 1`] = ` | |||
</div> | |||
`; | |||
exports[`should render correctly with hotspots: collapsed 1`] = ` | |||
<div | |||
className="hotspot-category HIGH" | |||
> | |||
<a | |||
className="hotspot-category-header display-flex-space-between display-flex-center" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<strong | |||
className="flex-1" | |||
> | |||
Class Injection | |||
</strong> | |||
<span> | |||
<span | |||
className="counter-badge" | |||
> | |||
2 | |||
</span> | |||
<ChevronDownIcon | |||
className="big-spacer-left" | |||
/> | |||
</span> | |||
</a> | |||
</div> | |||
`; | |||
exports[`should render correctly: empty 1`] = `""`; |
@@ -102,22 +102,23 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` | |||
<ul> | |||
<li | |||
className="spacer-bottom" | |||
key="cat1" | |||
key="cat2" | |||
> | |||
<HotspotCategory | |||
categoryKey="cat2" | |||
hotspots={ | |||
Array [ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"key": "h1", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "cat1", | |||
"securityCategory": "cat2", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
@@ -125,29 +126,46 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
startsExpanded={true} | |||
title="cat1" | |||
onToggleExpand={[Function]} | |||
selectedHotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
title="cat2" | |||
/> | |||
</li> | |||
<li | |||
className="spacer-bottom" | |||
key="cat2" | |||
key="cat1" | |||
> | |||
<HotspotCategory | |||
categoryKey="cat1" | |||
hotspots={ | |||
Array [ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h1", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "cat2", | |||
"securityCategory": "cat1", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
@@ -155,9 +173,25 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
startsExpanded={false} | |||
title="cat2" | |||
onToggleExpand={[Function]} | |||
selectedHotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
title="cat1" | |||
/> | |||
</li> | |||
</ul> | |||
@@ -184,6 +218,7 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` | |||
key="cat1" | |||
> | |||
<HotspotCategory | |||
categoryKey="cat1" | |||
hotspots={ | |||
Array [ | |||
Object { | |||
@@ -219,8 +254,24 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
startsExpanded={false} | |||
onToggleExpand={[Function]} | |||
selectedHotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
title="cat1" | |||
/> | |||
</li> | |||
@@ -229,6 +280,7 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` | |||
key="cat2" | |||
> | |||
<HotspotCategory | |||
categoryKey="cat2" | |||
hotspots={ | |||
Array [ | |||
Object { | |||
@@ -249,8 +301,24 @@ exports[`should render correctly with hotspots: no pagination 1`] = ` | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
startsExpanded={false} | |||
onToggleExpand={[Function]} | |||
selectedHotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
title="cat2" | |||
/> | |||
</li> | |||
@@ -299,22 +367,23 @@ exports[`should render correctly with hotspots: pagination 1`] = ` | |||
<ul> | |||
<li | |||
className="spacer-bottom" | |||
key="cat1" | |||
key="cat2" | |||
> | |||
<HotspotCategory | |||
categoryKey="cat2" | |||
hotspots={ | |||
Array [ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"key": "h1", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "cat1", | |||
"securityCategory": "cat2", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
@@ -322,29 +391,46 @@ exports[`should render correctly with hotspots: pagination 1`] = ` | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
startsExpanded={true} | |||
title="cat1" | |||
onToggleExpand={[Function]} | |||
selectedHotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
title="cat2" | |||
/> | |||
</li> | |||
<li | |||
className="spacer-bottom" | |||
key="cat2" | |||
key="cat1" | |||
> | |||
<HotspotCategory | |||
categoryKey="cat1" | |||
hotspots={ | |||
Array [ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h1", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "cat2", | |||
"securityCategory": "cat1", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
@@ -352,9 +438,25 @@ exports[`should render correctly with hotspots: pagination 1`] = ` | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
startsExpanded={false} | |||
title="cat2" | |||
onToggleExpand={[Function]} | |||
selectedHotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
title="cat1" | |||
/> | |||
</li> | |||
</ul> | |||
@@ -381,6 +483,7 @@ exports[`should render correctly with hotspots: pagination 1`] = ` | |||
key="cat1" | |||
> | |||
<HotspotCategory | |||
categoryKey="cat1" | |||
hotspots={ | |||
Array [ | |||
Object { | |||
@@ -416,8 +519,24 @@ exports[`should render correctly with hotspots: pagination 1`] = ` | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
startsExpanded={false} | |||
onToggleExpand={[Function]} | |||
selectedHotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
title="cat1" | |||
/> | |||
</li> | |||
@@ -426,6 +545,7 @@ exports[`should render correctly with hotspots: pagination 1`] = ` | |||
key="cat2" | |||
> | |||
<HotspotCategory | |||
categoryKey="cat2" | |||
hotspots={ | |||
Array [ | |||
Object { | |||
@@ -446,8 +566,24 @@ exports[`should render correctly with hotspots: pagination 1`] = ` | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
startsExpanded={false} | |||
onToggleExpand={[Function]} | |||
selectedHotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h2", | |||
"line": 81, | |||
"message": "'3' is a magic number.", | |||
"project": "com.github.kevinsawicki:http-request", | |||
"resolution": undefined, | |||
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", | |||
"securityCategory": "command-injection", | |||
"status": "TO_REVIEW", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
} | |||
} | |||
title="cat2" | |||
/> | |||
</li> |
@@ -19,7 +19,9 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { assignSecurityHotspot } from '../../../../api/security-hotspots'; | |||
import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage'; | |||
import { withCurrentUser } from '../../../../components/hoc/withCurrentUser'; | |||
import { isLoggedIn } from '../../../../helpers/users'; | |||
import { Hotspot, HotspotStatus } from '../../../../types/security-hotspots'; | |||
@@ -72,6 +74,11 @@ export class Assignee extends React.PureComponent<Props, State> { | |||
this.props.onAssigneeChange(); | |||
} | |||
}) | |||
.then(() => | |||
addGlobalSuccessMessage( | |||
translateWithParameters('hotspots.assign.success', newAssignee.name) | |||
) | |||
) | |||
.catch(() => this.setState({ loading: false })); | |||
} | |||
}; |
@@ -22,6 +22,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { assignSecurityHotspot } from '../../../../../api/security-hotspots'; | |||
import addGlobalSuccessMessage from '../../../../../app/utils/addGlobalSuccessMessage'; | |||
import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots'; | |||
import { mockCurrentUser, mockUser } from '../../../../../helpers/testMocks'; | |||
import { HotspotStatus } from '../../../../../types/security-hotspots'; | |||
@@ -32,6 +33,10 @@ jest.mock('../../../../../api/security-hotspots', () => ({ | |||
assignSecurityHotspot: jest.fn() | |||
})); | |||
jest.mock('../../../../../app/utils/addGlobalSuccessMessage', () => ({ | |||
default: jest.fn() | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect( | |||
@@ -78,6 +83,7 @@ it('should handle assign event correctly', async () => { | |||
loading: false | |||
}); | |||
expect(onAssigneeChange).toHaveBeenCalled(); | |||
expect(addGlobalSuccessMessage).toHaveBeenCalled(); | |||
}); | |||
function shallowRender(props?: Partial<Assignee['props']>) { |
@@ -18,7 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { setSecurityHotspotStatus } from '../../../../api/security-hotspots'; | |||
import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage'; | |||
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots'; | |||
import { | |||
getStatusAndResolutionFromStatusOption, | |||
@@ -86,6 +88,14 @@ export default class StatusSelection extends React.PureComponent<Props, State> { | |||
this.setState({ loading: false }); | |||
this.props.onStatusOptionChange(selectedStatus); | |||
}) | |||
.then(() => | |||
addGlobalSuccessMessage( | |||
translateWithParameters( | |||
'hotspots.update.success', | |||
translate('hotspots.status_option', selectedStatus) | |||
) | |||
) | |||
) | |||
.catch(() => this.setState({ loading: false })); | |||
} | |||
}; |
@@ -44,14 +44,11 @@ export function groupByCategory( | |||
) { | |||
const groups = groupBy(hotspots, h => h.securityCategory); | |||
return sortBy( | |||
Object.keys(groups).map(key => ({ | |||
key, | |||
title: getCategoryTitle(key, securityCategories), | |||
hotspots: groups[key] | |||
})), | |||
cat => cat.title | |||
); | |||
return Object.keys(groups).map(key => ({ | |||
key, | |||
title: getCategoryTitle(key, securityCategories), | |||
hotspots: groups[key] | |||
})); | |||
} | |||
export function sortHotspots( |
@@ -699,6 +699,9 @@ hotspot.filters.show_all=Show all hotspots | |||
hotspots.reviewed.tooltip=Percentage of Security Hotspots reviewed (fixed or safe) among all non-closed Security Hotspots. | |||
hotspots.review_hotspot=Review Hotspot | |||
hotspots.assign.success=Security Hotspot was successfully assigned to {0} | |||
hotspots.update.success=Security Hotspot status was successfully changed to {0} | |||
#------------------------------------------------------------------------------ | |||
# | |||
# ISSUES |