@@ -70,6 +70,7 @@ interface Props { | |||
onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; | |||
onLocationSelect: (index: number) => void; | |||
openIssuesByLine: Dict<boolean>; | |||
renderAdditionalChildInLine?: (lineNumber: number) => React.ReactNode | undefined; | |||
renderDuplicationPopup: (index: number, line: number) => React.ReactNode; | |||
scroll?: (element: HTMLElement, offset?: number) => void; | |||
snippet: SourceLine[]; | |||
@@ -153,6 +154,10 @@ export default class SnippetViewer extends React.PureComponent<Props> { | |||
return ( | |||
<Line | |||
additionalChild={ | |||
this.props.renderAdditionalChildInLine && | |||
this.props.renderAdditionalChildInLine(line.line) | |||
} | |||
branchLike={this.props.branchLike} | |||
displayAllIssues={false} | |||
displayCoverage={true} |
@@ -52,6 +52,28 @@ it('should render correctly with no SCM', () => { | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render additional child in line', () => { | |||
const sourceline = mockSourceLine({ line: 42 }); | |||
const child = <div>child</div>; | |||
const renderAdditionalChildInLine = jest.fn().mockReturnValue(child); | |||
const wrapper = shallowRender({ renderAdditionalChildInLine, snippet: [sourceline] }); | |||
const renderedLine = wrapper.instance().renderLine({ | |||
displayDuplications: false, | |||
index: 1, | |||
issuesForLine: [], | |||
issueLocations: [], | |||
line: sourceline, | |||
snippet: [sourceline], | |||
symbols: [], | |||
verticalBuffer: 5 | |||
}); | |||
expect(renderAdditionalChildInLine).toBeCalledWith(42); | |||
expect(renderedLine.props.additionalChild).toBe(child); | |||
}); | |||
it('should render correctly when at the top of the file', () => { | |||
const snippet = range(1, 8).map(line => mockSourceLine({ line })); | |||
const wrapper = shallowRender({ |
@@ -0,0 +1,38 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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. | |||
*/ | |||
.hotspot-primary-location { | |||
border: 1px solid var(--blue); | |||
background-color: var(--issueBgColor); | |||
border-left: 4px solid; | |||
margin: 10px -10px; | |||
} | |||
.hotspot-primary-location.hotspot-risk-exposure-HIGH { | |||
border-left-color: var(--red); | |||
} | |||
.hotspot-primary-location.hotspot-risk-exposure-MEDIUM { | |||
border-left-color: var(--orange); | |||
} | |||
.hotspot-primary-location.hotspot-risk-exposure-LOW { | |||
border-left-color: var(--yellow); | |||
} |
@@ -0,0 +1,47 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { ButtonLink } from '../../../components/controls/buttons'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
import './HotspotPrimaryLocationBox.css'; | |||
export interface HotspotPrimaryLocationBoxProps { | |||
hotspot: Hotspot; | |||
onCommentClick: () => void; | |||
} | |||
export default function HotspotPrimaryLocationBox(props: HotspotPrimaryLocationBoxProps) { | |||
const { hotspot } = props; | |||
return ( | |||
<div | |||
className={classNames( | |||
'hotspot-primary-location', | |||
'display-flex-space-between display-flex-center padded-top padded-bottom big-padded-left big-padded-right', | |||
`hotspot-risk-exposure-${hotspot.rule.vulnerabilityProbability}` | |||
)}> | |||
<div className="text-bold">{hotspot.message}</div> | |||
<ButtonLink className="nowrap big-spacer-left" onClick={props.onCommentClick}> | |||
{translate('hotspots.comment.open')} | |||
</ButtonLink> | |||
</div> | |||
); | |||
} |
@@ -32,6 +32,7 @@ interface Props { | |||
branchLike?: BranchLike; | |||
component: Component; | |||
hotspot: Hotspot; | |||
onCommentButtonClick: () => void; | |||
} | |||
interface State { | |||
@@ -175,6 +176,7 @@ export default class HotspotSnippetContainer extends React.Component<Props, Stat | |||
hotspot={hotspot} | |||
loading={loading} | |||
locations={locations} | |||
onCommentButtonClick={this.props.onCommentButtonClick} | |||
onExpandBlock={this.handleExpansion} | |||
onSymbolClick={this.handleSymbolClick} | |||
sourceLines={sourceLines} |
@@ -31,6 +31,7 @@ import { | |||
SourceViewerFile | |||
} from '../../../types/types'; | |||
import SnippetViewer from '../../issues/crossComponentSourceViewer/SnippetViewer'; | |||
import HotspotPrimaryLocationBox from './HotspotPrimaryLocationBox'; | |||
export interface HotspotSnippetContainerRendererProps { | |||
branchLike?: BranchLike; | |||
@@ -39,6 +40,7 @@ export interface HotspotSnippetContainerRendererProps { | |||
hotspot: Hotspot; | |||
loading: boolean; | |||
locations: { [line: number]: LinearIssueLocation[] }; | |||
onCommentButtonClick: () => void; | |||
onExpandBlock: (direction: ExpandDirection) => Promise<void>; | |||
onSymbolClick: (symbols: string[]) => void; | |||
sourceLines: SourceLine[]; | |||
@@ -61,6 +63,13 @@ export default function HotspotSnippetContainerRenderer( | |||
sourceViewerFile | |||
} = props; | |||
const renderHotspotBoxInLine = (lineNumber: number) => | |||
lineNumber === hotspot.line ? ( | |||
<HotspotPrimaryLocationBox hotspot={hotspot} onCommentClick={props.onCommentButtonClick} /> | |||
) : ( | |||
undefined | |||
); | |||
return ( | |||
<> | |||
{!loading && sourceLines.length === 0 && ( | |||
@@ -101,6 +110,7 @@ export default function HotspotSnippetContainerRenderer( | |||
onIssuePopupToggle={noop} | |||
onLocationSelect={noop} | |||
openIssuesByLine={{}} | |||
renderAdditionalChildInLine={renderHotspotBoxInLine} | |||
renderDuplicationPopup={noop} | |||
snippet={sourceLines} | |||
/> |
@@ -169,6 +169,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
branchLike={fillBranchLike(hotspot.project.branch, hotspot.project.pullRequest)} | |||
component={component} | |||
hotspot={hotspot} | |||
onCommentButtonClick={props.onShowCommentForm} | |||
/> | |||
<HotspotViewerTabs hotspot={hotspot} /> | |||
<HotspotReviewHistoryAndComments |
@@ -0,0 +1,57 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { ButtonLink } from '../../../../components/controls/buttons'; | |||
import { mockHotspot, mockHotspotRule } from '../../../../helpers/mocks/security-hotspots'; | |||
import { RiskExposure } from '../../../../types/security-hotspots'; | |||
import HotspotPrimaryLocationBox, { | |||
HotspotPrimaryLocationBoxProps | |||
} from '../HotspotPrimaryLocationBox'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it.each([[RiskExposure.HIGH], [RiskExposure.MEDIUM], [RiskExposure.LOW]])( | |||
'should indicate risk exposure: %s', | |||
vulnerabilityProbability => { | |||
const wrapper = shallowRender({ | |||
hotspot: mockHotspot({ rule: mockHotspotRule({ vulnerabilityProbability }) }) | |||
}); | |||
expect(wrapper.hasClass(`hotspot-risk-exposure-${vulnerabilityProbability}`)).toBe(true); | |||
} | |||
); | |||
it('should handle click', () => { | |||
const onCommentClick = jest.fn(); | |||
const wrapper = shallowRender({ onCommentClick }); | |||
wrapper.find(ButtonLink).simulate('click'); | |||
expect(onCommentClick).toBeCalled(); | |||
}); | |||
function shallowRender(props: Partial<HotspotPrimaryLocationBoxProps> = {}) { | |||
return shallow( | |||
<HotspotPrimaryLocationBox hotspot={mockHotspot()} onCommentClick={jest.fn()} {...props} /> | |||
); | |||
} |
@@ -205,6 +205,7 @@ function shallowRender(props?: Partial<HotspotSnippetContainer['props']>) { | |||
branchLike={branch} | |||
component={mockComponent()} | |||
hotspot={mockHotspot()} | |||
onCommentButtonClick={jest.fn()} | |||
{...props} | |||
/> | |||
); |
@@ -22,6 +22,7 @@ import * as React from 'react'; | |||
import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockSourceLine, mockSourceViewerFile } from '../../../../helpers/testMocks'; | |||
import SnippetViewer from '../../../issues/crossComponentSourceViewer/SnippetViewer'; | |||
import HotspotSnippetContainerRenderer, { | |||
HotspotSnippetContainerRendererProps | |||
} from '../HotspotSnippetContainerRenderer'; | |||
@@ -31,6 +32,18 @@ it('should render correctly', () => { | |||
expect(shallowRender({ sourceLines: [mockSourceLine()] })).toMatchSnapshot('with sourcelines'); | |||
}); | |||
it('should render a HotspotPrimaryLocationBox', () => { | |||
const wrapper = shallowRender({ | |||
hotspot: mockHotspot({ line: 42 }), | |||
sourceLines: [mockSourceLine()] | |||
}); | |||
const { renderAdditionalChildInLine } = wrapper.find(SnippetViewer).props(); | |||
expect(renderAdditionalChildInLine!(10)).toBeUndefined(); | |||
expect(renderAdditionalChildInLine!(42)).not.toBeUndefined(); | |||
}); | |||
function shallowRender(props?: Partial<HotspotSnippetContainerRendererProps>) { | |||
return shallow( | |||
<HotspotSnippetContainerRenderer | |||
@@ -40,6 +53,7 @@ function shallowRender(props?: Partial<HotspotSnippetContainerRendererProps>) { | |||
hotspot={mockHotspot()} | |||
loading={false} | |||
locations={{}} | |||
onCommentButtonClick={jest.fn()} | |||
onExpandBlock={jest.fn()} | |||
onSymbolClick={jest.fn()} | |||
sourceLines={[]} |
@@ -0,0 +1,19 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="hotspot-primary-location display-flex-space-between display-flex-center padded-top padded-bottom big-padded-left big-padded-right hotspot-risk-exposure-HIGH" | |||
> | |||
<div | |||
className="text-bold" | |||
> | |||
'3' is a magic number. | |||
</div> | |||
<ButtonLink | |||
className="nowrap big-spacer-left" | |||
onClick={[MockFunction]} | |||
> | |||
hotspots.comment.open | |||
</ButtonLink> | |||
</div> | |||
`; |
@@ -95,6 +95,7 @@ exports[`should render correctly 1`] = ` | |||
], | |||
} | |||
} | |||
onCommentButtonClick={[MockFunction]} | |||
onExpandBlock={[Function]} | |||
onSymbolClick={[Function]} | |||
sourceLines={Array []} |
@@ -219,6 +219,7 @@ exports[`should render correctly: with sourcelines 1`] = ` | |||
onIssuePopupToggle={[Function]} | |||
onLocationSelect={[Function]} | |||
openIssuesByLine={Object {}} | |||
renderAdditionalChildInLine={[Function]} | |||
renderDuplicationPopup={[Function]} | |||
snippet={ | |||
Array [ |
@@ -371,6 +371,7 @@ exports[`should render correctly: anonymous user 1`] = ` | |||
], | |||
} | |||
} | |||
onCommentButtonClick={[MockFunction]} | |||
/> | |||
<HotspotViewerTabs | |||
hotspot={ | |||
@@ -904,6 +905,7 @@ exports[`should render correctly: assignee without name 1`] = ` | |||
], | |||
} | |||
} | |||
onCommentButtonClick={[MockFunction]} | |||
/> | |||
<HotspotViewerTabs | |||
hotspot={ | |||
@@ -1437,6 +1439,7 @@ exports[`should render correctly: default 1`] = ` | |||
], | |||
} | |||
} | |||
onCommentButtonClick={[MockFunction]} | |||
/> | |||
<HotspotViewerTabs | |||
hotspot={ | |||
@@ -1970,6 +1973,7 @@ exports[`should render correctly: deleted assignee 1`] = ` | |||
], | |||
} | |||
} | |||
onCommentButtonClick={[MockFunction]} | |||
/> | |||
<HotspotViewerTabs | |||
hotspot={ | |||
@@ -2516,6 +2520,7 @@ exports[`should render correctly: show success modal 1`] = ` | |||
], | |||
} | |||
} | |||
onCommentButtonClick={[MockFunction]} | |||
/> | |||
<HotspotViewerTabs | |||
hotspot={ | |||
@@ -3049,6 +3054,7 @@ exports[`should render correctly: unassigned 1`] = ` | |||
], | |||
} | |||
} | |||
onCommentButtonClick={[MockFunction]} | |||
/> | |||
<HotspotViewerTabs | |||
hotspot={ |
@@ -31,6 +31,7 @@ import LineNumber from './LineNumber'; | |||
import LineSCM from './LineSCM'; | |||
interface Props { | |||
additionalChild?: React.ReactNode; | |||
branchLike: BranchLike | undefined; | |||
displayAllIssues?: boolean; | |||
displayCoverage: boolean; | |||
@@ -90,6 +91,7 @@ export default class Line extends React.PureComponent<Props> { | |||
render() { | |||
const { | |||
additionalChild, | |||
branchLike, | |||
displayAllIssues, | |||
displayCoverage, | |||
@@ -182,6 +184,7 @@ export default class Line extends React.PureComponent<Props> { | |||
)} | |||
<LineCode | |||
additionalChild={additionalChild} | |||
branchLike={branchLike} | |||
displayIssueLocationsCount={displayIssueLocationsCount} | |||
displayIssueLocationsLink={displayIssueLocationsLink} |
@@ -32,6 +32,7 @@ import { | |||
import LineIssuesList from './LineIssuesList'; | |||
interface Props { | |||
additionalChild?: React.ReactNode; | |||
branchLike: BranchLike | undefined; | |||
displayIssueLocationsCount?: boolean; | |||
displayIssueLocationsLink?: boolean; | |||
@@ -154,6 +155,7 @@ export default class LineCode extends React.PureComponent<Props, State> { | |||
render() { | |||
const { | |||
additionalChild, | |||
highlightedLocationMessage, | |||
highlightedSymbols, | |||
issues, | |||
@@ -253,6 +255,7 @@ export default class LineCode extends React.PureComponent<Props, State> { | |||
selectedIssue={selectedIssue} | |||
/> | |||
)} | |||
{additionalChild} | |||
</td> | |||
); | |||
} |
@@ -25,6 +25,9 @@ import LineCode from '../LineCode'; | |||
it('render code', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ additionalChild: <div>additional child</div> })).toMatchSnapshot( | |||
'with additional child' | |||
); | |||
}); | |||
function shallowRender(props: Partial<LineCode['props']> = {}) { |
@@ -119,3 +119,126 @@ exports[`render code 1`] = ` | |||
/> | |||
</td> | |||
`; | |||
exports[`render code: with additional child 1`] = ` | |||
<td | |||
className="source-line-code code has-issues" | |||
data-line-number={16} | |||
> | |||
<div | |||
className="source-line-code-inner" | |||
> | |||
<pre> | |||
<span | |||
className="k source-line-code-issue" | |||
key="0" | |||
> | |||
impor | |||
</span> | |||
<span | |||
className="k" | |||
key="1" | |||
> | |||
t | |||
</span> | |||
<span | |||
className="" | |||
key="2" | |||
> | |||
java.util. | |||
</span> | |||
<span | |||
className="sym-9 sym highlighted" | |||
key="3" | |||
> | |||
ArrayList | |||
</span> | |||
<span | |||
className="" | |||
key="4" | |||
> | |||
; | |||
</span> | |||
</pre> | |||
</div> | |||
<LineIssuesList | |||
branchLike={ | |||
Object { | |||
"analysisDate": "2018-01-01", | |||
"excludedFromPurge": true, | |||
"isMain": false, | |||
"name": "branch-6.7", | |||
} | |||
} | |||
issues={ | |||
Array [ | |||
Object { | |||
"actions": Array [], | |||
"component": "main.js", | |||
"componentLongName": "main.js", | |||
"componentQualifier": "FIL", | |||
"componentUuid": "foo1234", | |||
"creationDate": "2017-03-01T09:36:01+0100", | |||
"flows": Array [], | |||
"fromHotspot": false, | |||
"key": "issue-1", | |||
"line": 25, | |||
"message": "Reduce the number of conditional operators (4) used in the expression", | |||
"project": "myproject", | |||
"projectKey": "foo", | |||
"projectName": "Foo", | |||
"rule": "javascript:S1067", | |||
"ruleName": "foo", | |||
"secondaryLocations": Array [], | |||
"severity": "MAJOR", | |||
"status": "OPEN", | |||
"textRange": Object { | |||
"endLine": 26, | |||
"endOffset": 15, | |||
"startLine": 25, | |||
"startOffset": 0, | |||
}, | |||
"transitions": Array [], | |||
"type": "BUG", | |||
}, | |||
Object { | |||
"actions": Array [], | |||
"component": "main.js", | |||
"componentLongName": "main.js", | |||
"componentQualifier": "FIL", | |||
"componentUuid": "foo1234", | |||
"creationDate": "2017-03-01T09:36:01+0100", | |||
"flows": Array [], | |||
"fromHotspot": false, | |||
"key": "issue-2", | |||
"line": 25, | |||
"message": "Reduce the number of conditional operators (4) used in the expression", | |||
"project": "myproject", | |||
"projectKey": "foo", | |||
"projectName": "Foo", | |||
"rule": "javascript:S1067", | |||
"ruleName": "foo", | |||
"secondaryLocations": Array [], | |||
"severity": "MAJOR", | |||
"status": "OPEN", | |||
"textRange": Object { | |||
"endLine": 26, | |||
"endOffset": 15, | |||
"startLine": 25, | |||
"startOffset": 0, | |||
}, | |||
"transitions": Array [], | |||
"type": "BUG", | |||
}, | |||
] | |||
} | |||
onIssueChange={[MockFunction]} | |||
onIssueClick={[MockFunction]} | |||
onIssuePopupToggle={[MockFunction]} | |||
selectedIssue="issue-1" | |||
/> | |||
<div> | |||
additional child | |||
</div> | |||
</td> | |||
`; |
@@ -741,7 +741,7 @@ hotspots.tabs.fix_recommendations=How can you fix it? | |||
hotspots.review_history.created=created Security Hotspot | |||
hotspots.review_history.comment_added=added a comment | |||
hotspots.comment.field=Comment: | |||
hotspots.comment.open=Add Comment | |||
hotspots.comment.open=Comment | |||
hotspots.comment.submit=Comment | |||
hotspots.open_in_ide.open=Open in IDE | |||
hotspots.open_in_ide.success=Success. Switch to your IDE to see the security hotspot. |