Browse Source

SONAR-16007 Add hotspot box to primary location

tags/9.4.0.54424
Jeremy Davis 2 years ago
parent
commit
9620694f92
19 changed files with 357 additions and 1 deletions
  1. 5
    0
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
  2. 22
    0
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx
  3. 38
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.css
  4. 47
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx
  5. 2
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx
  6. 10
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
  7. 1
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
  8. 57
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotPrimaryLocationBox-test.tsx
  9. 1
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx
  10. 14
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx
  11. 19
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotPrimaryLocationBox-test.tsx.snap
  12. 1
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap
  13. 1
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
  14. 6
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
  15. 3
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
  16. 3
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
  17. 3
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx
  18. 123
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap
  19. 1
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 5
- 0
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx View File

@@ -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}

+ 22
- 0
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx View File

@@ -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({

+ 38
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.css View File

@@ -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);
}

+ 47
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx View File

@@ -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>
);
}

+ 2
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx View File

@@ -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}

+ 10
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx View File

@@ -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}
/>

+ 1
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx View File

@@ -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

+ 57
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotPrimaryLocationBox-test.tsx View File

@@ -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} />
);
}

+ 1
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx View File

@@ -205,6 +205,7 @@ function shallowRender(props?: Partial<HotspotSnippetContainer['props']>) {
branchLike={branch}
component={mockComponent()}
hotspot={mockHotspot()}
onCommentButtonClick={jest.fn()}
{...props}
/>
);

+ 14
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx View File

@@ -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={[]}

+ 19
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotPrimaryLocationBox-test.tsx.snap View File

@@ -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>
`;

+ 1
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap View File

@@ -95,6 +95,7 @@ exports[`should render correctly 1`] = `
],
}
}
onCommentButtonClick={[MockFunction]}
onExpandBlock={[Function]}
onSymbolClick={[Function]}
sourceLines={Array []}

+ 1
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap View File

@@ -219,6 +219,7 @@ exports[`should render correctly: with sourcelines 1`] = `
onIssuePopupToggle={[Function]}
onLocationSelect={[Function]}
openIssuesByLine={Object {}}
renderAdditionalChildInLine={[Function]}
renderDuplicationPopup={[Function]}
snippet={
Array [

+ 6
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap View File

@@ -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={

+ 3
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx View File

@@ -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}

+ 3
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx View File

@@ -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>
);
}

+ 3
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx View File

@@ -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']> = {}) {

+ 123
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap View File

@@ -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>
`;

+ 1
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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.

Loading…
Cancel
Save