Browse Source

SONAR-11887 Inverse issue icons and coverage/duplication information in codeviewer gutter

* Display issues type icons instead of severity icons in codeviewer gutter
tags/7.8
Wouter Admiraal 5 years ago
parent
commit
d3712c692f
16 changed files with 1173 additions and 85 deletions
  1. 8
    8
      server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
  2. 4
    4
      server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx
  3. 129
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx
  4. 24
    40
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx
  5. 800
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap
  6. 13
    13
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap
  7. 7
    2
      server/sonar-web/src/main/js/components/SourceViewer/styles.css
  8. 45
    0
      server/sonar-web/src/main/js/components/icons-components/IssueIcon.tsx
  9. 33
    0
      server/sonar-web/src/main/js/components/icons-components/__tests__/IssueIcon-test.tsx
  10. 9
    0
      server/sonar-web/src/main/js/components/icons-components/__tests__/__snapshots__/IssueIcon-test.tsx.snap
  11. 10
    14
      server/sonar-web/src/main/js/components/ui/IssueTypeIcon.tsx
  12. 34
    0
      server/sonar-web/src/main/js/components/ui/__tests__/IssueTypeIcon-test.tsx
  13. 29
    0
      server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IssueTypeIcon-test.tsx.snap
  14. 15
    1
      server/sonar-web/src/main/js/helpers/__tests__/issues-test.ts
  15. 3
    3
      server/sonar-web/src/main/js/helpers/issues.ts
  16. 10
    0
      server/sonar-web/src/main/js/helpers/testMocks.ts

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

@@ -123,7 +123,13 @@ export default class Line extends React.PureComponent<Props> {
previousLine={this.props.previousLine}
/>

{this.props.displayCoverage && <LineCoverage line={line} />}
{this.props.displayIssues && !this.props.displayAllIssues && (
<LineIssuesIndicator
issues={this.props.issues}
line={line}
onClick={this.handleIssuesIndicatorClick}
/>
)}

{this.props.displayDuplications && (
<LineDuplications line={line} onClick={this.props.loadDuplications} />
@@ -141,13 +147,7 @@ export default class Line extends React.PureComponent<Props> {
/>
))}

{this.props.displayIssues && !this.props.displayAllIssues && (
<LineIssuesIndicator
issues={this.props.issues}
line={line}
onClick={this.handleIssuesIndicatorClick}
/>
)}
{this.props.displayCoverage && <LineCoverage line={line} />}

<LineCode
branchLike={this.props.branchLike}

+ 4
- 4
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx View File

@@ -19,8 +19,8 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
import SeverityIcon from '../../icons-components/SeverityIcon';
import { sortBySeverity } from '../../../helpers/issues';
import IssueIcon from '../../icons-components/IssueIcon';
import { sortByType } from '../../../helpers/issues';

interface Props {
issues: T.Issue[];
@@ -40,7 +40,7 @@ export default class LineIssuesIndicator extends React.PureComponent<Props> {
const className = classNames('source-meta', 'source-line-issues', {
'source-line-with-issues': hasIssues
});
const mostImportantIssue = hasIssues ? sortBySeverity(issues)[0] : null;
const mostImportantIssue = hasIssues ? sortByType(issues)[0] : null;

return (
<td
@@ -49,7 +49,7 @@ export default class LineIssuesIndicator extends React.PureComponent<Props> {
onClick={hasIssues ? this.handleClick : undefined}
role={hasIssues ? 'button' : undefined}
tabIndex={hasIssues ? 0 : undefined}>
{mostImportantIssue != null && <SeverityIcon severity={mostImportantIssue.severity} />}
{mostImportantIssue != null && <IssueIcon type={mostImportantIssue.type} />}
{issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
</td>
);

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

@@ -0,0 +1,129 @@
/*
* 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 Line from '../Line';
import { mockPullRequest, mockSourceLine, mockIssue } from '../../../../helpers/testMocks';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should render correctly for last, new, and highlighted lines', () => {
expect(
shallowRender({
highlighted: true,
last: true,
line: mockSourceLine({ isNew: true })
})
).toMatchSnapshot();
});

it('should render correctly with coverage', () => {
expect(
shallowRender({
displayCoverage: true
})
).toMatchSnapshot();
});

it('should render correctly with duplication information', () => {
expect(
shallowRender({
displayDuplications: true,
duplicationsCount: 3
})
).toMatchSnapshot();
});

it('should render correctly with issues info', () => {
expect(shallowRender({ displayIssues: true })).toMatchSnapshot();
});

it('handles the opening and closing of issues', () => {
const line = mockSourceLine();
const issue = mockIssue();
const onIssuesClose = jest.fn();
const onIssueUnselect = jest.fn();
const onIssuesOpen = jest.fn();
const onIssueSelect = jest.fn();
const wrapper = shallowRender({
issues: [issue],
line,
onIssuesClose,
onIssueSelect,
onIssuesOpen,
onIssueUnselect,
openIssues: true
});
const instance = wrapper.instance();

instance.handleIssuesIndicatorClick();
expect(onIssuesClose).toBeCalledWith(line);
expect(onIssueUnselect).toBeCalled();

wrapper.setProps({ openIssues: false });
instance.handleIssuesIndicatorClick();
expect(onIssuesOpen).toBeCalledWith(line);
expect(onIssueSelect).toBeCalledWith(issue.key);
});

function shallowRender(props: Partial<Line['props']> = {}) {
return shallow<Line>(
<Line
branchLike={mockPullRequest()}
displayAllIssues={false}
displayCoverage={false}
displayDuplications={false}
displayIssueLocationsCount={false}
displayIssueLocationsLink={false}
displayIssues={false}
displayLocationMarkers={false}
duplications={[]}
duplicationsCount={0}
highlighted={false}
highlightedLocationMessage={undefined}
highlightedSymbols={undefined}
issueLocations={[]}
issuePopup={undefined}
issues={[mockIssue(), mockIssue(false, { type: 'VULNERABILITY' })]}
last={false}
line={mockSourceLine()}
linePopup={undefined}
loadDuplications={jest.fn()}
onLinePopupToggle={jest.fn()}
onIssueChange={jest.fn()}
onIssuePopupToggle={jest.fn()}
onIssuesClose={jest.fn()}
onIssueSelect={jest.fn()}
onIssuesOpen={jest.fn()}
onIssueUnselect={jest.fn()}
onLocationSelect={jest.fn()}
onSymbolClick={jest.fn()}
openIssues={false}
previousLine={undefined}
renderDuplicationPopup={jest.fn()}
scroll={jest.fn()}
secondaryIssueLocations={[]}
selectedIssue={undefined}
{...props}
/>
);
}

+ 24
- 40
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx View File

@@ -19,56 +19,40 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { click } from '../../../../helpers/testUtils';
import LineIssuesIndicator from '../LineIssuesIndicator';
import { click } from '../../../../helpers/testUtils';
import { mockIssue } from '../../../../helpers/testMocks';

const issueBase: T.Issue = {
actions: [],
component: '',
componentLongName: '',
componentQualifier: '',
componentUuid: '',
creationDate: '',
key: '',
flows: [],
fromHotspot: false,
message: '',
organization: '',
project: '',
projectName: '',
projectOrganization: '',
projectKey: '',
rule: '',
ruleName: '',
secondaryLocations: [],
severity: '',
status: '',
transitions: [],
type: 'BUG'
};

it('render highest severity', () => {
const line = { line: 3 };
const issues = [
{ ...issueBase, key: 'foo', severity: 'MINOR' },
{ ...issueBase, key: 'bar', severity: 'CRITICAL' }
];
it('should render correctly', () => {
const onClick = jest.fn();
const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
const wrapper = shallowRender({ onClick });
expect(wrapper).toMatchSnapshot();

click(wrapper);
expect(onClick).toHaveBeenCalled();

const nextIssues = [{ severity: 'MINOR' }, { severity: 'INFO' }];
const nextIssues = [
mockIssue(false, { key: 'foo', type: 'VULNERABILITY' }),
mockIssue(false, { key: 'bar', type: 'SECURITY_HOTSPOT' })
];
wrapper.setProps({ issues: nextIssues });
expect(wrapper).toMatchSnapshot();
});

it('no issues', () => {
const line = { line: 3 };
const issues: T.Issue[] = [];
const onClick = jest.fn();
const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
expect(wrapper).toMatchSnapshot();
it('should render correctly for no issues', () => {
expect(shallowRender({ issues: [] })).toMatchSnapshot();
});

function shallowRender(props: Partial<LineIssuesIndicator['props']> = {}) {
return shallow(
<LineIssuesIndicator
issues={[
mockIssue(false, { key: 'foo', type: 'CODE_SMELL' }),
mockIssue(false, { key: 'bar', type: 'BUG' })
]}
line={{ line: 3 }}
onClick={jest.fn()}
{...props}
/>
);
}

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

@@ -0,0 +1,800 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<tr
className="source-line"
data-line-number={5}
>
<LineNumber
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineSCM
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineCode
branchLike={
Object {
"analysisDate": "2018-01-01",
"base": "master",
"branch": "feature/foo/bar",
"key": "1001",
"target": "master",
"title": "Foo Bar feature",
}
}
displayIssueLocationsCount={false}
displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"rule": "javascript:S1067",
"ruleName": "foo",
"secondaryLocations": Array [],
"severity": "MAJOR",
"status": "OPEN",
"textRange": Object {
"endLine": 26,
"endOffset": 15,
"startLine": 25,
"startOffset": 0,
},
"transitions": Array [],
"type": "VULNERABILITY",
},
]
}
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
scroll={[MockFunction]}
secondaryIssueLocations={Array []}
showIssues={false}
/>
</tr>
`;

exports[`should render correctly for last, new, and highlighted lines 1`] = `
<tr
className="source-line source-line-highlighted source-line-filtered source-line-last"
data-line-number={5}
>
<LineNumber
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"isNew": true,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineSCM
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"isNew": true,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineCode
branchLike={
Object {
"analysisDate": "2018-01-01",
"base": "master",
"branch": "feature/foo/bar",
"key": "1001",
"target": "master",
"title": "Foo Bar feature",
}
}
displayIssueLocationsCount={false}
displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"rule": "javascript:S1067",
"ruleName": "foo",
"secondaryLocations": Array [],
"severity": "MAJOR",
"status": "OPEN",
"textRange": Object {
"endLine": 26,
"endOffset": 15,
"startLine": 25,
"startOffset": 0,
},
"transitions": Array [],
"type": "VULNERABILITY",
},
]
}
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"isNew": true,
"line": 5,
}
}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
scroll={[MockFunction]}
secondaryIssueLocations={Array []}
showIssues={false}
/>
</tr>
`;

exports[`should render correctly with coverage 1`] = `
<tr
className="source-line"
data-line-number={5}
>
<LineNumber
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineSCM
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineCoverage
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
/>
<LineCode
branchLike={
Object {
"analysisDate": "2018-01-01",
"base": "master",
"branch": "feature/foo/bar",
"key": "1001",
"target": "master",
"title": "Foo Bar feature",
}
}
displayIssueLocationsCount={false}
displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"rule": "javascript:S1067",
"ruleName": "foo",
"secondaryLocations": Array [],
"severity": "MAJOR",
"status": "OPEN",
"textRange": Object {
"endLine": 26,
"endOffset": 15,
"startLine": 25,
"startOffset": 0,
},
"transitions": Array [],
"type": "VULNERABILITY",
},
]
}
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
scroll={[MockFunction]}
secondaryIssueLocations={Array []}
showIssues={false}
/>
</tr>
`;

exports[`should render correctly with duplication information 1`] = `
<tr
className="source-line"
data-line-number={5}
>
<LineNumber
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineSCM
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineDuplications
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onClick={[MockFunction]}
/>
<LineDuplicationBlock
duplicated={false}
index={0}
key="0"
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
renderDuplicationPopup={[MockFunction]}
/>
<LineDuplicationBlock
duplicated={false}
index={1}
key="1"
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
renderDuplicationPopup={[MockFunction]}
/>
<LineDuplicationBlock
duplicated={false}
index={2}
key="2"
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
renderDuplicationPopup={[MockFunction]}
/>
<LineCode
branchLike={
Object {
"analysisDate": "2018-01-01",
"base": "master",
"branch": "feature/foo/bar",
"key": "1001",
"target": "master",
"title": "Foo Bar feature",
}
}
displayIssueLocationsCount={false}
displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"rule": "javascript:S1067",
"ruleName": "foo",
"secondaryLocations": Array [],
"severity": "MAJOR",
"status": "OPEN",
"textRange": Object {
"endLine": 26,
"endOffset": 15,
"startLine": 25,
"startOffset": 0,
},
"transitions": Array [],
"type": "VULNERABILITY",
},
]
}
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
scroll={[MockFunction]}
secondaryIssueLocations={Array []}
showIssues={false}
/>
</tr>
`;

exports[`should render correctly with issues info 1`] = `
<tr
className="source-line"
data-line-number={5}
>
<LineNumber
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineSCM
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
<LineIssuesIndicator
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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"rule": "javascript:S1067",
"ruleName": "foo",
"secondaryLocations": Array [],
"severity": "MAJOR",
"status": "OPEN",
"textRange": Object {
"endLine": 26,
"endOffset": 15,
"startLine": 25,
"startOffset": 0,
},
"transitions": Array [],
"type": "VULNERABILITY",
},
]
}
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onClick={[Function]}
/>
<LineCode
branchLike={
Object {
"analysisDate": "2018-01-01",
"base": "master",
"branch": "feature/foo/bar",
"key": "1001",
"target": "master",
"title": "Foo Bar feature",
}
}
displayIssueLocationsCount={false}
displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"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": "AVsae-CQS-9G3txfbFN2",
"line": 25,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"project": "myproject",
"projectKey": "foo",
"projectName": "Foo",
"projectOrganization": "org",
"rule": "javascript:S1067",
"ruleName": "foo",
"secondaryLocations": Array [],
"severity": "MAJOR",
"status": "OPEN",
"textRange": Object {
"endLine": 26,
"endOffset": 15,
"startLine": 25,
"startOffset": 0,
},
"transitions": Array [],
"type": "VULNERABILITY",
},
]
}
line={
Object {
"code": "function fooBar() {",
"coverageStatus": "covered",
"coveredConditions": 2,
"line": 5,
}
}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
scroll={[MockFunction]}
secondaryIssueLocations={Array []}
showIssues={false}
/>
</tr>
`;

+ 13
- 13
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap View File

@@ -1,13 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`no issues 1`] = `
<td
className="source-meta source-line-issues"
data-line-number={3}
/>
`;

exports[`render highest severity 1`] = `
exports[`should render correctly 1`] = `
<td
className="source-meta source-line-issues source-line-with-issues"
data-line-number={3}
@@ -15,8 +8,8 @@ exports[`render highest severity 1`] = `
role="button"
tabIndex={0}
>
<SeverityIcon
severity="CRITICAL"
<IssueIcon
type="BUG"
/>
<span
className="source-line-issues-counter"
@@ -26,7 +19,7 @@ exports[`render highest severity 1`] = `
</td>
`;

exports[`render highest severity 2`] = `
exports[`should render correctly 2`] = `
<td
className="source-meta source-line-issues source-line-with-issues"
data-line-number={3}
@@ -34,8 +27,8 @@ exports[`render highest severity 2`] = `
role="button"
tabIndex={0}
>
<SeverityIcon
severity="MINOR"
<IssueIcon
type="VULNERABILITY"
/>
<span
className="source-line-issues-counter"
@@ -44,3 +37,10 @@ exports[`render highest severity 2`] = `
</span>
</td>
`;

exports[`should render correctly for no issues 1`] = `
<td
className="source-meta source-line-issues"
data-line-number={3}
/>
`;

+ 7
- 2
server/sonar-web/src/main/js/components/SourceViewer/styles.css View File

@@ -174,14 +174,19 @@
position: relative;
padding: 0 2px;
background-color: var(--barBackgroundColor);
white-space: nowrap;
}

.source-line-with-issues {
padding-right: 4px;
}

.source-line-issues-counter {
position: absolute;
top: -1px;
right: -1px;
left: 17px;
line-height: 8px;
font-size: 8px;
z-index: 900;
}

.source-line-coverage {

+ 45
- 0
server/sonar-web/src/main/js/components/icons-components/IssueIcon.tsx View File

@@ -0,0 +1,45 @@
/*
* 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 BugIcon from '../icons-components/BugIcon';
import VulnerabilityIcon from '../icons-components/VulnerabilityIcon';
import CodeSmellIcon from '../icons-components/CodeSmellIcon';
import SecurityHotspotIcon from '../icons-components/SecurityHotspotIcon';

interface Props {
className?: string;
type: T.IssueType;
size?: number;
}

export default function IssueIcon({ className, type, size }: Props) {
switch (type) {
case 'BUG':
return <BugIcon className={className} size={size} />;
case 'VULNERABILITY':
return <VulnerabilityIcon className={className} size={size} />;
case 'CODE_SMELL':
return <CodeSmellIcon className={className} size={size} />;
case 'SECURITY_HOTSPOT':
return <SecurityHotspotIcon className={className} size={size} />;
default:
return null;
}
}

+ 33
- 0
server/sonar-web/src/main/js/components/icons-components/__tests__/IssueIcon-test.tsx View File

@@ -0,0 +1,33 @@
/*
* 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 IssueIcon from '../IssueIcon';

it('should render correctly', () => {
expect(shallowRender('BUG')).toMatchSnapshot();
expect(shallowRender('VULNERABILITY')).toMatchSnapshot();
expect(shallowRender('CODE_SMELL')).toMatchSnapshot();
expect(shallowRender('SECURITY_HOTSPOT')).toMatchSnapshot();
});

function shallowRender(type: T.IssueType) {
return shallow(<IssueIcon type={type} />);
}

+ 9
- 0
server/sonar-web/src/main/js/components/icons-components/__tests__/__snapshots__/IssueIcon-test.tsx.snap View File

@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `<BugIcon />`;

exports[`should render correctly 2`] = `<VulnerabilityIcon />`;

exports[`should render correctly 3`] = `<CodeSmellIcon />`;

exports[`should render correctly 4`] = `<SecurityHotspotIcon />`;

+ 10
- 14
server/sonar-web/src/main/js/components/ui/IssueTypeIcon.tsx View File

@@ -18,45 +18,41 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import BugIcon from '../icons-components/BugIcon';
import VulnerabilityIcon from '../icons-components/VulnerabilityIcon';
import CodeSmellIcon from '../icons-components/CodeSmellIcon';
import SecurityHotspotIcon from '../icons-components/SecurityHotspotIcon';
import IssueIcon from '../icons-components/IssueIcon';

interface Props {
export interface Props {
className?: string;
query: string;
size?: number;
}

export default function IssueTypeIcon({ className, query, size }: Props) {
let icon;
let type: T.IssueType;

switch (query.toLowerCase()) {
case 'bug':
case 'bugs':
case 'new_bugs':
icon = <BugIcon size={size} />;
type = 'BUG';
break;
case 'vulnerability':
case 'vulnerabilities':
case 'new_vulnerabilities':
icon = <VulnerabilityIcon size={size} />;
type = 'VULNERABILITY';
break;
case 'code_smell':
case 'code_smells':
case 'new_code_smells':
icon = <CodeSmellIcon size={size} />;
type = 'CODE_SMELL';
break;
case 'security_hotspot':
case 'security_hotspots':
icon = <SecurityHotspotIcon size={size} />;
type = 'SECURITY_HOTSPOT';
break;
default:
return null;
}

if (!icon) {
return null;
}

const icon = <IssueIcon size={size} type={type} />;
return className ? <span className={className}>{icon}</span> : icon;
}

+ 34
- 0
server/sonar-web/src/main/js/components/ui/__tests__/IssueTypeIcon-test.tsx View File

@@ -0,0 +1,34 @@
/*
* 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 IssueTypeIcon, { Props } from '../IssueTypeIcon';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ className: 'my-class', query: 'security_hotspots' })).toMatchSnapshot();
expect(shallowRender({ query: 'new_code_smells' })).toMatchSnapshot();
expect(shallowRender({ query: 'vulnerability' })).toMatchSnapshot();
expect(shallowRender({ query: 'unknown' }).type()).toBe(null);
});

function shallowRender(props: Partial<Props> = {}) {
return shallow(<IssueTypeIcon query="bugs" {...props} />);
}

+ 29
- 0
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IssueTypeIcon-test.tsx.snap View File

@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<IssueIcon
type="BUG"
/>
`;

exports[`should render correctly 2`] = `
<span
className="my-class"
>
<IssueIcon
type="SECURITY_HOTSPOT"
/>
</span>
`;

exports[`should render correctly 3`] = `
<IssueIcon
type="CODE_SMELL"
/>
`;

exports[`should render correctly 4`] = `
<IssueIcon
type="VULNERABILITY"
/>
`;

+ 15
- 1
server/sonar-web/src/main/js/helpers/__tests__/issues-test.ts View File

@@ -17,7 +17,21 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { parseIssueFromResponse } from '../issues';
import { parseIssueFromResponse, sortByType } from '../issues';
import { mockIssue } from '../testMocks';

it('should sort issues correctly by type', () => {
const bug1 = mockIssue(false, { type: 'BUG', key: 'bug1' });
const bug2 = mockIssue(false, { type: 'BUG', key: 'bug2' });
const codeSmell = mockIssue(false, { type: 'CODE_SMELL', key: 'code_smell' });
const vulnerability1 = mockIssue(false, { type: 'VULNERABILITY', key: 'vulnerability1' });
const vulnerability2 = mockIssue(false, { type: 'VULNERABILITY', key: 'vulnerability2' });
const securityHotspot = mockIssue(false, { type: 'SECURITY_HOTSPOT', key: 'security_hotspot' });

expect(
sortByType([bug1, codeSmell, bug2, securityHotspot, vulnerability1, vulnerability2])
).toEqual([bug1, bug2, vulnerability1, vulnerability2, codeSmell, securityHotspot]);
});

it('should populate comments data', () => {
const users = [

+ 3
- 3
server/sonar-web/src/main/js/helpers/issues.ts View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { flatten, sortBy } from 'lodash';
import { SEVERITIES } from './constants';
import { ISSUE_TYPES } from './constants';

interface Comment {
login: string;
@@ -59,8 +59,8 @@ export interface RawIssue extends IssueBase {
textRange?: T.TextRange;
}

export function sortBySeverity(issues: T.Issue[]): T.Issue[] {
return sortBy(issues, issue => SEVERITIES.indexOf(issue.severity));
export function sortByType(issues: T.Issue[]): T.Issue[] {
return sortBy(issues, issue => ISSUE_TYPES.indexOf(issue.type));
}

function injectRelational(

+ 10
- 0
server/sonar-web/src/main/js/helpers/testMocks.ts View File

@@ -477,6 +477,16 @@ export function mockStore(state: any = {}, reducer = (state: any) => state): Sto
return createStore(reducer, state);
}

export function mockSourceLine(overrides: Partial<T.SourceLine> = {}): T.SourceLine {
return {
code: 'function fooBar() {',
coverageStatus: 'covered',
coveredConditions: 2,
line: 5,
...overrides
};
}

export function mockDocumentationEntry(
overrides: Partial<DocumentationEntry> = {}
): DocumentationEntry {

Loading…
Cancel
Save