aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>2022-02-16 10:41:39 +0100
committersonartech <sonartech@sonarsource.com>2022-02-25 20:02:54 +0000
commite5474111c8f3e985cfa751d8cd86f1950aaf8e4d (patch)
tree5164d6200ea226175def9007343bd1948507c40a /server
parent9620694f92f2525837ce822d69f1296bf003ae69 (diff)
downloadsonarqube-e5474111c8f3e985cfa751d8cd86f1950aaf8e4d.tar.gz
sonarqube-e5474111c8f3e985cfa751d8cd86f1950aaf8e4d.zip
SONAR-16007 Showing secondary locations in hotspot list box
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.tsx76
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationsNavigator-test.tsx121
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap163
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationsNavigator-test.tsx.snap270
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap112
-rw-r--r--server/sonar-web/src/main/js/apps/issues/styles.css89
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts44
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css4
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap42
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/utils.ts52
-rw-r--r--server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.css98
-rw-r--r--server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx (renamed from server/sonar-web/src/main/js/apps/issues/conciseIssuesList/CrossFileLocationsNavigator.tsx)57
-rw-r--r--server/sonar-web/src/main/js/components/locations/LocationsList.tsx70
-rw-r--r--server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.css27
-rw-r--r--server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx (renamed from server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.tsx)12
-rw-r--r--server/sonar-web/src/main/js/components/locations/__tests__/CrossFileLocationsNavigator-test.tsx (renamed from server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/CrossFileLocationsNavigator-test.tsx)28
-rw-r--r--server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx75
-rw-r--r--server/sonar-web/src/main/js/components/locations/__tests__/SingleFileLocationsNavigator-test.tsx (renamed from server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationsNavigatorLocation-test.tsx)6
-rw-r--r--server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap112
-rw-r--r--server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap58
-rw-r--r--server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationsNavigatorLocation-test.tsx.snap)4
-rw-r--r--server/sonar-web/src/main/js/types/security-hotspots.ts3
28 files changed, 714 insertions, 876 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
index 0080b942dc6..2411146ab9c 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
@@ -18,11 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
+import { uniq } from 'lodash';
import * as React from 'react';
+import LocationsList from '../../../components/locations/LocationsList';
import TypeHelper from '../../../components/shared/TypeHelper';
import { Issue } from '../../../types/types';
+import { getLocations } from '../utils';
import ConciseIssueLocations from './ConciseIssueLocations';
-import ConciseIssueLocationsNavigator from './ConciseIssueLocationsNavigator';
interface Props {
issue: Issue;
@@ -75,7 +77,7 @@ export default class ConciseIssueBox extends React.PureComponent<Props> {
};
render() {
- const { issue, selected } = this.props;
+ const { issue, selected, selectedFlowIndex, selectedLocationIndex } = this.props;
const clickAttributesMain = selected
? {}
@@ -85,6 +87,11 @@ export default class ConciseIssueBox extends React.PureComponent<Props> {
? { onClick: this.handleClick, role: 'listitem', tabIndex: 0 }
: {};
+ const locations = getLocations(issue, selectedFlowIndex);
+
+ const locationComponents = [issue.component, ...locations.map(location => location.component)];
+ const isCrossFile = uniq(locationComponents).length > 1;
+
return (
<div
className={classNames('concise-issue-box', 'clearfix', { selected })}
@@ -101,16 +108,18 @@ export default class ConciseIssueBox extends React.PureComponent<Props> {
<ConciseIssueLocations
issue={issue}
onFlowSelect={this.props.onFlowSelect}
- selectedFlowIndex={this.props.selectedFlowIndex}
+ selectedFlowIndex={selectedFlowIndex}
/>
</div>
{selected && (
- <ConciseIssueLocationsNavigator
- issue={issue}
+ <LocationsList
+ locations={locations}
+ uniqueKey={issue.key}
+ isCrossFile={isCrossFile}
onLocationSelect={this.props.onLocationSelect}
scroll={this.props.scroll}
- selectedFlowIndex={this.props.selectedFlowIndex}
- selectedLocationIndex={this.props.selectedLocationIndex}
+ selectedFlowIndex={selectedFlowIndex}
+ selectedLocationIndex={selectedLocationIndex}
/>
)}
</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.tsx
deleted file mode 100644
index f633d402937..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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 { uniq } from 'lodash';
-import * as React from 'react';
-import { Issue } from '../../../types/types';
-import { getLocations } from '../utils';
-import ConciseIssueLocationsNavigatorLocation from './ConciseIssueLocationsNavigatorLocation';
-import CrossFileLocationsNavigator from './CrossFileLocationsNavigator';
-
-interface Props {
- issue: Pick<Issue, 'component' | 'key' | 'flows' | 'secondaryLocations' | 'type'>;
- onLocationSelect: (index: number) => void;
- scroll: (element: Element) => void;
- selectedFlowIndex: number | undefined;
- selectedLocationIndex: number | undefined;
-}
-
-export default class ConciseIssueLocationsNavigator extends React.PureComponent<Props> {
- render() {
- const locations = getLocations(this.props.issue, this.props.selectedFlowIndex);
-
- if (!locations || locations.length === 0 || locations.every(location => !location.msg)) {
- return null;
- }
-
- const locationComponents = [
- this.props.issue.component,
- ...locations.map(location => location.component)
- ];
- const isCrossFile = uniq(locationComponents).length > 1;
-
- if (isCrossFile) {
- return (
- <CrossFileLocationsNavigator
- issue={this.props.issue}
- locations={locations}
- onLocationSelect={this.props.onLocationSelect}
- scroll={this.props.scroll}
- selectedLocationIndex={this.props.selectedLocationIndex}
- />
- );
- } else {
- return (
- <div className="concise-issue-locations-navigator spacer-top">
- {locations.map((location, index) => (
- <ConciseIssueLocationsNavigatorLocation
- index={index}
- key={index}
- message={location.msg}
- onClick={this.props.onLocationSelect}
- scroll={this.props.scroll}
- selected={index === this.props.selectedLocationIndex}
- />
- ))}
- </div>
- );
- }
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationsNavigator-test.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationsNavigator-test.tsx
deleted file mode 100644
index 2b5509ec5a4..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationsNavigator-test.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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 { mockIssue } from '../../../../helpers/testMocks';
-import { FlowLocation } from '../../../../types/types';
-import ConciseIssueLocationsNavigator from '../ConciseIssueLocationsNavigator';
-
-const location1: FlowLocation = {
- component: 'foo',
- componentName: 'src/foo.js',
- msg: 'Do not use foo',
- textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 }
-};
-
-const location2: FlowLocation = {
- component: 'foo',
- componentName: 'src/foo.js',
- msg: 'Do not use foo',
- textRange: { startLine: 8, endLine: 8, startOffset: 0, endOffset: 5 }
-};
-
-const location3: FlowLocation = {
- component: 'bar',
- componentName: 'src/bar.js',
- msg: 'Do not use bar',
- textRange: { startLine: 15, endLine: 16, startOffset: 4, endOffset: 6 }
-};
-
-it('should render secondary locations in the same file', () => {
- const issue = mockIssue(false, {
- component: 'foo',
- key: '',
- flows: [],
- secondaryLocations: [location1, location2]
- });
- expect(shallowRender({ issue })).toMatchSnapshot();
-});
-
-it('should render flow locations in the same file', () => {
- const issue = mockIssue(false, {
- component: 'foo',
- key: '',
- flows: [[location1, location2]],
- secondaryLocations: []
- });
- expect(shallowRender({ issue })).toMatchSnapshot();
-});
-
-it('should render selected flow locations in the same file', () => {
- const issue = mockIssue(false, {
- component: 'foo',
- key: '',
- flows: [[location1, location2]],
- secondaryLocations: [location1]
- });
- expect(shallowRender({ issue, selectedFlowIndex: 0 })).toMatchSnapshot();
-});
-
-it('should render flow locations in different file', () => {
- const issue = mockIssue(false, {
- component: 'foo',
- key: '',
- flows: [[location1, location3]],
- secondaryLocations: []
- });
- expect(shallowRender({ issue })).toMatchSnapshot();
-});
-
-it('should not render locations', () => {
- const issue = mockIssue(false, {
- component: 'foo',
- key: '',
- flows: [],
- secondaryLocations: []
- });
- const wrapper = shallowRender({ issue });
- expect(wrapper.type()).toBeNull();
-});
-
-it('should render taint analysis issues correctly', () => {
- const issue = mockIssue(false, {
- component: 'foo',
- key: '',
- flows: [[location1, location2, location3]],
- secondaryLocations: [],
- type: 'VULNERABILITY'
- });
-
- expect(shallowRender({ issue })).toMatchSnapshot();
-});
-
-function shallowRender(overrides: Partial<ConciseIssueLocationsNavigator['props']> = {}) {
- return shallow<ConciseIssueLocationsNavigator>(
- <ConciseIssueLocationsNavigator
- issue={mockIssue()}
- onLocationSelect={jest.fn()}
- scroll={jest.fn()}
- selectedFlowIndex={undefined}
- selectedLocationIndex={undefined}
- {...overrides}
- />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap
index 6947b89f047..5badb12ee28 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap
@@ -55,42 +55,14 @@ exports[`should render correctly 1`] = `
selectedFlowIndex={0}
/>
</div>
- <ConciseIssueLocationsNavigator
- issue={
- 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",
- "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",
- }
- }
+ <LocationsList
+ isCrossFile={false}
+ locations={Array []}
onLocationSelect={[MockFunction]}
scroll={[MockFunction]}
selectedFlowIndex={0}
selectedLocationIndex={0}
+ uniqueKey="AVsae-CQS-9G3txfbFN2"
/>
</div>
`;
@@ -219,111 +191,44 @@ exports[`should render correctly 2`] = `
selectedFlowIndex={0}
/>
</div>
- <ConciseIssueLocationsNavigator
- issue={
- Object {
- "actions": Array [],
- "component": "main.js",
- "componentLongName": "main.js",
- "componentQualifier": "FIL",
- "componentUuid": "foo1234",
- "creationDate": "2017-03-01T09:36:01+0100",
- "flows": Array [
- Array [
- Object {
- "component": "main.js",
- "textRange": Object {
- "endLine": 2,
- "endOffset": 2,
- "startLine": 1,
- "startOffset": 1,
- },
- },
- Object {
- "component": "main.js",
- "textRange": Object {
- "endLine": 2,
- "endOffset": 2,
- "startLine": 1,
- "startOffset": 1,
- },
- },
- Object {
- "component": "main.js",
- "textRange": Object {
- "endLine": 2,
- "endOffset": 2,
- "startLine": 1,
- "startOffset": 1,
- },
- },
- ],
- Array [
- Object {
- "component": "main.js",
- "textRange": Object {
- "endLine": 2,
- "endOffset": 2,
- "startLine": 1,
- "startOffset": 1,
- },
- },
- Object {
- "component": "main.js",
- "textRange": Object {
- "endLine": 2,
- "endOffset": 2,
- "startLine": 1,
- "startOffset": 1,
- },
- },
- ],
- ],
- "fromHotspot": false,
- "key": "AVsae-CQS-9G3txfbFN2",
- "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 [
- Object {
- "component": "main.js",
- "textRange": Object {
- "endLine": 2,
- "endOffset": 2,
- "startLine": 1,
- "startOffset": 1,
- },
+ <LocationsList
+ isCrossFile={false}
+ locations={
+ Array [
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
},
- Object {
- "component": "main.js",
- "textRange": Object {
- "endLine": 2,
- "endOffset": 2,
- "startLine": 1,
- "startOffset": 1,
- },
+ },
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
},
- ],
- "severity": "MAJOR",
- "status": "OPEN",
- "textRange": Object {
- "endLine": 26,
- "endOffset": 15,
- "startLine": 25,
- "startOffset": 0,
},
- "transitions": Array [],
- "type": "BUG",
- }
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ ]
}
onLocationSelect={[MockFunction]}
scroll={[MockFunction]}
selectedFlowIndex={0}
selectedLocationIndex={0}
+ uniqueKey="AVsae-CQS-9G3txfbFN2"
/>
</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationsNavigator-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationsNavigator-test.tsx.snap
deleted file mode 100644
index 0434816e9e4..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationsNavigator-test.tsx.snap
+++ /dev/null
@@ -1,270 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render flow locations in different file 1`] = `
-<CrossFileLocationsNavigator
- issue={
- Object {
- "actions": Array [],
- "component": "foo",
- "componentLongName": "main.js",
- "componentQualifier": "FIL",
- "componentUuid": "foo1234",
- "creationDate": "2017-03-01T09:36:01+0100",
- "flows": Array [
- Array [
- Object {
- "component": "foo",
- "componentName": "src/foo.js",
- "msg": "Do not use foo",
- "textRange": Object {
- "endLine": 7,
- "endOffset": 8,
- "startLine": 7,
- "startOffset": 5,
- },
- },
- Object {
- "component": "bar",
- "componentName": "src/bar.js",
- "msg": "Do not use bar",
- "textRange": Object {
- "endLine": 16,
- "endOffset": 6,
- "startLine": 15,
- "startOffset": 4,
- },
- },
- ],
- ],
- "fromHotspot": false,
- "key": "",
- "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",
- }
- }
- locations={
- Array [
- Object {
- "component": "foo",
- "componentName": "src/foo.js",
- "msg": "Do not use foo",
- "textRange": Object {
- "endLine": 7,
- "endOffset": 8,
- "startLine": 7,
- "startOffset": 5,
- },
- },
- Object {
- "component": "bar",
- "componentName": "src/bar.js",
- "msg": "Do not use bar",
- "textRange": Object {
- "endLine": 16,
- "endOffset": 6,
- "startLine": 15,
- "startOffset": 4,
- },
- },
- ]
- }
- onLocationSelect={[MockFunction]}
- scroll={[MockFunction]}
-/>
-`;
-
-exports[`should render flow locations in the same file 1`] = `
-<div
- className="concise-issue-locations-navigator spacer-top"
->
- <ConciseIssueLocationsNavigatorLocation
- index={0}
- key="0"
- message="Do not use foo"
- onClick={[MockFunction]}
- scroll={[MockFunction]}
- selected={false}
- />
- <ConciseIssueLocationsNavigatorLocation
- index={1}
- key="1"
- message="Do not use foo"
- onClick={[MockFunction]}
- scroll={[MockFunction]}
- selected={false}
- />
-</div>
-`;
-
-exports[`should render secondary locations in the same file 1`] = `
-<div
- className="concise-issue-locations-navigator spacer-top"
->
- <ConciseIssueLocationsNavigatorLocation
- index={0}
- key="0"
- message="Do not use foo"
- onClick={[MockFunction]}
- scroll={[MockFunction]}
- selected={false}
- />
- <ConciseIssueLocationsNavigatorLocation
- index={1}
- key="1"
- message="Do not use foo"
- onClick={[MockFunction]}
- scroll={[MockFunction]}
- selected={false}
- />
-</div>
-`;
-
-exports[`should render selected flow locations in the same file 1`] = `
-<div
- className="concise-issue-locations-navigator spacer-top"
->
- <ConciseIssueLocationsNavigatorLocation
- index={0}
- key="0"
- message="Do not use foo"
- onClick={[MockFunction]}
- scroll={[MockFunction]}
- selected={false}
- />
- <ConciseIssueLocationsNavigatorLocation
- index={1}
- key="1"
- message="Do not use foo"
- onClick={[MockFunction]}
- scroll={[MockFunction]}
- selected={false}
- />
-</div>
-`;
-
-exports[`should render taint analysis issues correctly 1`] = `
-<CrossFileLocationsNavigator
- issue={
- Object {
- "actions": Array [],
- "component": "foo",
- "componentLongName": "main.js",
- "componentQualifier": "FIL",
- "componentUuid": "foo1234",
- "creationDate": "2017-03-01T09:36:01+0100",
- "flows": Array [
- Array [
- Object {
- "component": "foo",
- "componentName": "src/foo.js",
- "msg": "Do not use foo",
- "textRange": Object {
- "endLine": 7,
- "endOffset": 8,
- "startLine": 7,
- "startOffset": 5,
- },
- },
- Object {
- "component": "foo",
- "componentName": "src/foo.js",
- "msg": "Do not use foo",
- "textRange": Object {
- "endLine": 8,
- "endOffset": 5,
- "startLine": 8,
- "startOffset": 0,
- },
- },
- Object {
- "component": "bar",
- "componentName": "src/bar.js",
- "msg": "Do not use bar",
- "textRange": Object {
- "endLine": 16,
- "endOffset": 6,
- "startLine": 15,
- "startOffset": 4,
- },
- },
- ],
- ],
- "fromHotspot": false,
- "key": "",
- "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": "VULNERABILITY",
- }
- }
- locations={
- Array [
- Object {
- "component": "foo",
- "componentName": "src/foo.js",
- "msg": "Do not use foo",
- "textRange": Object {
- "endLine": 7,
- "endOffset": 8,
- "startLine": 7,
- "startOffset": 5,
- },
- },
- Object {
- "component": "foo",
- "componentName": "src/foo.js",
- "msg": "Do not use foo",
- "textRange": Object {
- "endLine": 8,
- "endOffset": 5,
- "startLine": 8,
- "startOffset": 0,
- },
- },
- Object {
- "component": "bar",
- "componentName": "src/bar.js",
- "msg": "Do not use bar",
- "textRange": Object {
- "endLine": 16,
- "endOffset": 6,
- "startLine": 15,
- "startOffset": 4,
- },
- },
- ]
- }
- onLocationSelect={[MockFunction]}
- scroll={[MockFunction]}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap
deleted file mode 100644
index fd78ea74a5c..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap
+++ /dev/null
@@ -1,112 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div
- className="concise-issue-locations-navigator spacer-top"
->
- <div
- className="concise-issue-locations-navigator-file"
- key="0"
- >
- <div
- className="concise-issue-location-file"
- >
- <i
- className="concise-issue-location-file-circle little-spacer-right"
- />
- src/foo.js
- </div>
- <div
- className="concise-issue-location-file-locations"
- >
- <ConciseIssueLocationsNavigatorLocation
- index={0}
- key="0"
- message="Do not use foo"
- onClick={[MockFunction]}
- scroll={[MockFunction]}
- selected={false}
- />
- </div>
- </div>
- <div
- className="concise-issue-locations-navigator-file"
- >
- <div
- className="concise-issue-location-file"
- >
- <i
- className="concise-issue-location-file-circle-multiple little-spacer-right"
- />
- <a
- className="concise-issue-location-file-more"
- href="#"
- onClick={[Function]}
- >
- issues.x_more_locations.1
- </a>
- </div>
- </div>
- <div
- className="concise-issue-locations-navigator-file"
- key="1"
- >
- <div
- className="concise-issue-location-file"
- >
- <i
- className="concise-issue-location-file-circle little-spacer-right"
- />
- src/bar.js
- </div>
- <div
- className="concise-issue-location-file-locations"
- >
- <ConciseIssueLocationsNavigatorLocation
- index={2}
- key="2"
- message="Do not use bar"
- onClick={[MockFunction]}
- scroll={[MockFunction]}
- selected={false}
- />
- </div>
- </div>
-</div>
-`;
-
-exports[`should render locations with no component name 1`] = `
-<div
- className="concise-issue-locations-navigator spacer-top"
->
- <div
- className="concise-issue-locations-navigator-file"
- key="0"
- >
- <div
- className="concise-issue-location-file"
- >
- <i
- className="concise-issue-location-file-circle little-spacer-right"
- />
- </div>
- <div
- className="concise-issue-location-file-locations"
- >
- <ConciseIssueLocationsNavigatorLocation
- index={0}
- key="0"
- onClick={[MockFunction]}
- scroll={[MockFunction]}
- selected={false}
- />
- </div>
- </div>
-</div>
-`;
-
-exports[`should render with no locations 1`] = `
-<div
- className="concise-issue-locations-navigator spacer-top"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css
index d8cfea885cb..16f0981c472 100644
--- a/server/sonar-web/src/main/js/apps/issues/styles.css
+++ b/server/sonar-web/src/main/js/apps/issues/styles.css
@@ -137,95 +137,6 @@
color: white;
}
-.concise-issue-locations-navigator-location {
- position: relative;
- z-index: var(--aboveNormalZIndex);
- display: inline-flex;
- align-items: flex-start;
- max-width: 100%;
- border: none;
-}
-
-.concise-issue-locations-navigator-file {
- position: relative;
-}
-
-.concise-issue-locations-navigator-file + .concise-issue-locations-navigator-file {
- margin-top: calc(1.5 * var(--gridSize));
-}
-
-.concise-issue-locations-navigator-file:not(:last-child)::before {
- position: absolute;
- display: block;
- width: 0;
- top: 13px;
- bottom: calc(-2 * var(--gridSize));
- left: 4px;
- border-left: 1px dotted var(--conciseIssueRed);
- content: '';
-}
-
-.concise-issue-location-file {
- height: calc(2 * var(--gridSize));
- padding-bottom: calc(0.5 * var(--gridSize));
- font-size: var(--smallFontSize);
- font-weight: bold;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.concise-issue-location-file-circle,
-.concise-issue-location-file-circle-multiple,
-.concise-issue-location-file-circle-multiple::before,
-.concise-issue-location-file-circle-multiple::after {
- position: relative;
- top: 1px;
- display: inline-block;
- width: calc(1px + var(--gridSize));
- height: calc(1px + var(--gridSize));
- border: 1px solid var(--conciseIssueRed);
- border-radius: 100%;
- box-sizing: border-box;
- background-color: var(--issueBgColor);
-}
-
-.concise-issue-location-file-circle-multiple {
- top: -2px;
-}
-
-.concise-issue-location-file-circle-multiple::before {
- position: absolute;
- z-index: calc(5 + var(--normalZIndex));
- top: 2px;
- left: -1px;
- content: '';
-}
-
-.concise-issue-location-file-circle-multiple::after {
- position: absolute;
- z-index: calc(5 + var(--aboveNormalZIndex));
- top: 5px;
- left: -1px;
- content: '';
-}
-
-.concise-issue-location-file-locations {
- padding-left: calc(2 * var(--gridSize));
-}
-
-.concise-issue-location-file-more {
- border-color: rgba(209, 133, 130, 0.2);
- color: rgb(209, 133, 130) !important;
- font-style: italic;
- font-weight: normal;
-}
-
-.concise-issue-location-file-more:hover,
-.concise-issue-location-file-more:focus {
- border-color: rgba(209, 133, 130, 0.6);
-}
-
.component-source-container {
border: 1px solid var(--gray80);
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
index 969217f5617..f90fc16bf4b 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
@@ -74,7 +74,8 @@ interface State {
loading: boolean;
loadingMeasure: boolean;
loadingMore: boolean;
- selectedHotspot: RawHotspot | undefined;
+ selectedHotspot?: RawHotspot;
+ selectedHotspotLocation?: number;
standards: Standards;
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
index 715ce19cbff..43f521eb9e4 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
@@ -60,7 +60,8 @@ export interface SecurityHotspotsAppRendererProps {
onShowAllHotspots: () => void;
onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
onUpdateHotspot: (hotspotKey: string) => Promise<void>;
- selectedHotspot: RawHotspot | undefined;
+ selectedHotspot?: RawHotspot;
+ selectedHotspotLocation?: number;
securityCategories: StandardSecurityCategories;
standards: Standards;
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
index d8155826feb..12856d774dd 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
@@ -24,12 +24,14 @@ import {
HotspotStatus,
HotspotStatusFilter,
HotspotStatusOption,
+ RawHotspot,
ReviewHistoryType,
RiskExposure
} from '../../../types/security-hotspots';
-import { IssueChangelog } from '../../../types/types';
+import { FlowLocation, IssueChangelog } from '../../../types/types';
import {
getHotspotReviewHistory,
+ getLocations,
getStatusAndResolutionFromStatusOption,
getStatusFilterFromStatusOption,
getStatusOptionFromStatusAndResolution,
@@ -277,3 +279,43 @@ describe('getStatusFilterFromStatusOption', () => {
);
});
});
+
+describe('getLocations', () => {
+ it('should return the correct value', () => {
+ const location1: FlowLocation = {
+ component: 'foo',
+ msg: 'Do not use foo',
+ textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 }
+ };
+
+ const location2: FlowLocation = {
+ component: 'foo2',
+ msg: 'Do not use foo2',
+ textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 }
+ };
+
+ let rawFlows: RawHotspot['flows'] = [
+ {
+ locations: [location1]
+ }
+ ];
+ expect(getLocations(rawFlows, undefined)).toEqual([location1]);
+
+ rawFlows = [
+ {
+ locations: [location1, location2]
+ }
+ ];
+ expect(getLocations(rawFlows, undefined)).toEqual([location2, location1]);
+
+ rawFlows = [
+ {
+ locations: [location1, location2]
+ },
+ {
+ locations: []
+ }
+ ];
+ expect(getLocations(rawFlows, 0)).toEqual([location2, location1]);
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
index 4d2f505c4f2..fa176838b89 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
@@ -31,6 +31,7 @@ export interface HotspotCategoryProps {
onHotspotClick: (hotspot: RawHotspot) => void;
onToggleExpand?: (categoryKey: string, value: boolean) => void;
selectedHotspot: RawHotspot;
+ selectedHotspotLocation?: number;
title: string;
isLastAndIncomplete: boolean;
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css
index 482112477b2..8fa66008174 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css
@@ -96,3 +96,7 @@
.hotspot-risk-badge.LOW {
background-color: var(--yellow);
}
+
+.hotspot-box-filename {
+ direction: rtl;
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
index 8c78d98ab5e..afa5194f180 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
@@ -39,6 +39,7 @@ interface Props {
onLoadMore: () => void;
securityCategories: StandardSecurityCategories;
selectedHotspot: RawHotspot;
+ selectedHotspotLocation?: number;
statusFilter: HotspotStatusFilter;
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
index ed241c168dc..7865080337c 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
@@ -19,9 +19,11 @@
*/
import classNames from 'classnames';
import * as React from 'react';
-import { translate } from '../../../helpers/l10n';
+import QualifierIcon from '../../../components/icons/QualifierIcon';
+import { ComponentQualifier } from '../../../types/component';
+import { getFilePath, getLocations } from '../utils';
+import LocationsList from '../../../components/locations/LocationsList';
import { RawHotspot } from '../../../types/security-hotspots';
-import { getStatusOptionFromStatusAndResolution } from '../utils';
export interface HotspotListItemProps {
hotspot: RawHotspot;
@@ -31,16 +33,36 @@ export interface HotspotListItemProps {
export default function HotspotListItem(props: HotspotListItemProps) {
const { hotspot, selected } = props;
+ const locations = getLocations(hotspot.flows, undefined);
+ const path = getFilePath(hotspot.component, hotspot.project);
+
return (
<a
className={classNames('hotspot-item', { highlight: selected })}
href="#"
onClick={() => !selected && props.onClick(hotspot)}>
- <div className="little-spacer-left">{hotspot.message}</div>
- <div className="badge spacer-top">
- {translate(
- 'hotspots.status_option',
- getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution)
+ <div className="little-spacer-left text-bold">{hotspot.message}</div>
+ <div className="display-flex-center">
+ <QualifierIcon qualifier={ComponentQualifier.File} />
+ <div
+ className="little-spacer-left hotspot-box-filename text-ellipsis big-spacer-top big-spacer-bottom"
+ title={path}>
+ {path}
+ </div>
+ </div>
+ <div className="spacer-top">
+ {selected && (
+ <LocationsList
+ locations={locations}
+ isCrossFile={false} // Currently we are not supporting cross file for security hotspot
+ uniqueKey={hotspot.key}
+ onLocationSelect={() => {
+ /* noop */
+ }}
+ scroll={() => {
+ /* noop */
+ }}
+ />
)}
</div>
</a>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
index b959dae2d97..0d0f163550e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
@@ -7,15 +7,26 @@ exports[`should render correctly 1`] = `
onClick={[Function]}
>
<div
- className="little-spacer-left"
+ className="little-spacer-left text-bold"
>
'3' is a magic number.
</div>
<div
- className="badge spacer-top"
+ className="display-flex-center"
>
- hotspots.status_option.TO_REVIEW
+ <QualifierIcon
+ qualifier="FIL"
+ />
+ <div
+ className="little-spacer-left hotspot-box-filename text-ellipsis big-spacer-top big-spacer-bottom"
+ title="com.github.kevinsawicki.http.HttpRequest"
+ >
+ com.github.kevinsawicki.http.HttpRequest
+ </div>
</div>
+ <div
+ className="spacer-top"
+ />
</a>
`;
@@ -26,14 +37,33 @@ exports[`should render correctly 2`] = `
onClick={[Function]}
>
<div
- className="little-spacer-left"
+ className="little-spacer-left text-bold"
>
'3' is a magic number.
</div>
<div
- className="badge spacer-top"
+ className="display-flex-center"
+ >
+ <QualifierIcon
+ qualifier="FIL"
+ />
+ <div
+ className="little-spacer-left hotspot-box-filename text-ellipsis big-spacer-top big-spacer-bottom"
+ title="com.github.kevinsawicki.http.HttpRequest"
+ >
+ com.github.kevinsawicki.http.HttpRequest
+ </div>
+ </div>
+ <div
+ className="spacer-top"
>
- hotspots.status_option.TO_REVIEW
+ <LocationsList
+ isCrossFile={false}
+ locations={Array []}
+ onLocationSelect={[Function]}
+ scroll={[Function]}
+ uniqueKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ />
</div>
</a>
`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
index b50a669065c..cdcdd8cae45 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { groupBy, sortBy } from 'lodash';
+import { flatten, groupBy, sortBy } from 'lodash';
import {
renderCWECategory,
renderOwaspTop10Category,
@@ -36,7 +36,12 @@ import {
ReviewHistoryType,
RiskExposure
} from '../../types/security-hotspots';
-import { Dict, SourceViewerFile, StandardSecurityCategories } from '../../types/types';
+import {
+ Dict,
+ FlowLocation,
+ SourceViewerFile,
+ StandardSecurityCategories
+} from '../../types/types';
export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW];
export const SECURITY_STANDARDS = [
@@ -190,3 +195,46 @@ const STATUS_OPTION_TO_STATUS_FILTER = {
export function getStatusFilterFromStatusOption(statusOption: HotspotStatusOption) {
return STATUS_OPTION_TO_STATUS_FILTER[statusOption];
}
+
+function getSecondaryLocations(flows: RawHotspot['flows']) {
+ const parsedFlows: FlowLocation[][] = (flows || [])
+ .filter(flow => flow.locations !== undefined)
+ .map(flow => flow.locations!.filter(location => location.textRange != null))
+ .map(flow =>
+ flow.map(location => {
+ return { ...location };
+ })
+ );
+
+ const onlySecondaryLocations = parsedFlows.every(flow => flow.length === 1);
+
+ return onlySecondaryLocations
+ ? { secondaryLocations: orderLocations(flatten(parsedFlows)), flows: [] }
+ : { secondaryLocations: [], flows: parsedFlows.map(reverseLocations) };
+}
+
+export function getLocations(rawFlows: RawHotspot['flows'], selectedFlowIndex: number | undefined) {
+ const { flows, secondaryLocations } = getSecondaryLocations(rawFlows);
+ if (selectedFlowIndex !== undefined) {
+ return flows[selectedFlowIndex] || [];
+ }
+ return flows.length > 0 ? flows[0] : secondaryLocations;
+}
+
+function orderLocations(locations: FlowLocation[]) {
+ return sortBy(
+ locations,
+ location => location.textRange && location.textRange.startLine,
+ location => location.textRange && location.textRange.startOffset
+ );
+}
+
+function reverseLocations(locations: FlowLocation[]): FlowLocation[] {
+ const x = [...locations];
+ x.reverse();
+ return x;
+}
+
+export function getFilePath(component: string, project: string) {
+ return component.replace(project, '').replace(':', '');
+}
diff --git a/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.css b/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.css
new file mode 100644
index 00000000000..42f917e964a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.css
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+.locations-navigator-file {
+ position: relative;
+}
+
+.locations-navigator-file + .locations-navigator-file {
+ margin-top: calc(1.5 * var(--gridSize));
+}
+
+.locations-navigator-file:not(:last-child)::before {
+ position: absolute;
+ display: block;
+ width: 0;
+ top: 13px;
+ bottom: calc(-2 * var(--gridSize));
+ left: 4px;
+ border-left: 1px dotted var(--conciseIssueRed);
+ content: '';
+}
+
+.location-file-locations {
+ padding-left: calc(2 * var(--gridSize));
+}
+
+.location-file {
+ height: calc(2 * var(--gridSize));
+ padding-bottom: calc(0.5 * var(--gridSize));
+ font-size: var(--smallFontSize);
+ font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.location-file-circle,
+.location-file-circle-multiple,
+.location-file-circle-multiple::before,
+.location-file-circle-multiple::after {
+ position: relative;
+ top: 1px;
+ display: inline-block;
+ width: calc(1px + var(--gridSize));
+ height: calc(1px + var(--gridSize));
+ border: 1px solid var(--conciseIssueRed);
+ border-radius: 100%;
+ box-sizing: border-box;
+ background-color: var(--issueBgColor);
+}
+
+.location-file-circle-multiple {
+ top: -2px;
+}
+
+.location-file-circle-multiple::before {
+ position: absolute;
+ z-index: calc(5 + var(--normalZIndex));
+ top: 2px;
+ left: -1px;
+ content: '';
+}
+
+.location-file-circle-multiple::after {
+ position: absolute;
+ z-index: calc(5 + var(--aboveNormalZIndex));
+ top: 5px;
+ left: -1px;
+ content: '';
+}
+
+.location-file-more {
+ border-color: rgba(209, 133, 130, 0.2);
+ color: rgb(209, 133, 130) !important;
+ font-style: italic;
+ font-weight: normal;
+}
+
+.location-file-more:hover,
+.location-file-more:focus {
+ border-color: rgba(209, 133, 130, 0.6);
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/CrossFileLocationsNavigator.tsx b/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx
index a25c64fbfde..bf6074b5a54 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/CrossFileLocationsNavigator.tsx
+++ b/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx
@@ -18,13 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { translateWithParameters } from '../../../helpers/l10n';
-import { collapsePath } from '../../../helpers/path';
-import { FlowLocation, Issue } from '../../../types/types';
-import ConciseIssueLocationsNavigatorLocation from './ConciseIssueLocationsNavigatorLocation';
+import { translateWithParameters } from '../../helpers/l10n';
+import { collapsePath } from '../../helpers/path';
+import SingleFileLocationNavigator from './SingleFileLocationNavigator';
+import './CrossFileLocationNavigator.css';
+import { FlowLocation } from '../../types/types';
interface Props {
- issue: Pick<Issue, 'key' | 'type'>;
+ uniqueKey: string;
locations: FlowLocation[];
onLocationSelect: (index: number) => void;
scroll: (element: Element) => void;
@@ -44,11 +45,11 @@ interface LocationGroup {
const MAX_PATH_LENGTH = 15;
-export default class CrossFileLocationsNavigator extends React.PureComponent<Props, State> {
+export default class CrossFileLocationNavigator extends React.PureComponent<Props, State> {
state: State = { collapsed: true };
componentWillReceiveProps(nextProps: Props) {
- if (nextProps.issue.key !== this.props.issue.key) {
+ if (nextProps.uniqueKey !== this.props.uniqueKey) {
this.setState({ collapsed: true });
}
@@ -112,7 +113,7 @@ export default class CrossFileLocationsNavigator extends React.PureComponent<Pro
renderLocation = (index: number, message: string | undefined) => {
return (
- <ConciseIssueLocationsNavigatorLocation
+ <SingleFileLocationNavigator
index={index}
key={index}
message={message}
@@ -131,13 +132,13 @@ export default class CrossFileLocationsNavigator extends React.PureComponent<Pro
const { firstLocationIndex } = group;
const lastLocationIndex = group.locations.length - 1;
return (
- <div className="concise-issue-locations-navigator-file" key={groupIndex}>
- <div className="concise-issue-location-file">
- <i className="concise-issue-location-file-circle little-spacer-right" />
+ <div className="locations-navigator-file" key={groupIndex}>
+ <div className="location-file">
+ <i className="location-file-circle little-spacer-right" />
{collapsePath(group.componentName || '', MAX_PATH_LENGTH)}
</div>
{group.locations.length > 0 && (
- <div className="concise-issue-location-file-locations">
+ <div className="location-file-locations">
{onlyFirst && this.renderLocation(firstLocationIndex, group.locations[0].msg)}
{onlyLast &&
@@ -160,33 +161,33 @@ export default class CrossFileLocationsNavigator extends React.PureComponent<Pro
render() {
const { locations } = this.props;
const groups = this.groupByFile(locations);
+ const MIN_LOCATION_LENGTH = 2;
- if (locations.length > 2 && groups.length > 1 && this.state.collapsed) {
+ if (locations.length > MIN_LOCATION_LENGTH && groups.length > 1 && this.state.collapsed) {
const firstGroup = groups[0];
const lastGroup = groups[groups.length - 1];
return (
- <div className="concise-issue-locations-navigator spacer-top">
+ <div className="spacer-top">
{this.renderGroup(firstGroup, 0, { onlyFirst: true })}
- <div className="concise-issue-locations-navigator-file">
- <div className="concise-issue-location-file">
- <i className="concise-issue-location-file-circle-multiple little-spacer-right" />
- <a
- className="concise-issue-location-file-more"
- href="#"
- onClick={this.handleMoreLocationsClick}>
- {translateWithParameters('issues.x_more_locations', locations.length - 2)}
+ <div className="locations-navigator-file">
+ <div className="location-file">
+ <i className="location-file-circle-multiple little-spacer-right" />
+ <a className="location-file-more" href="#" onClick={this.handleMoreLocationsClick}>
+ {translateWithParameters(
+ 'issues.x_more_locations',
+ locations.length - MIN_LOCATION_LENGTH
+ )}
</a>
</div>
</div>
{this.renderGroup(lastGroup, groups.length - 1, { onlyLast: true })}
</div>
);
- } else {
- return (
- <div className="concise-issue-locations-navigator spacer-top">
- {groups.map((group, groupIndex) => this.renderGroup(group, groupIndex))}
- </div>
- );
}
+ return (
+ <div className="spacer-top">
+ {groups.map((group, groupIndex) => this.renderGroup(group, groupIndex))}
+ </div>
+ );
}
}
diff --git a/server/sonar-web/src/main/js/components/locations/LocationsList.tsx b/server/sonar-web/src/main/js/components/locations/LocationsList.tsx
new file mode 100644
index 00000000000..6a38b047a05
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/locations/LocationsList.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 * as React from 'react';
+import { FlowLocation } from '../../types/types';
+import CrossFileLocationNavigator from './CrossFileLocationNavigator';
+import SingleFileLocationNavigator from './SingleFileLocationNavigator';
+
+interface Props {
+ isCrossFile: boolean;
+ uniqueKey: string;
+ locations: FlowLocation[];
+ onLocationSelect: (index: number) => void;
+ scroll: (element: Element) => void;
+ selectedFlowIndex?: number;
+ selectedLocationIndex?: number;
+}
+
+export default class LocationsList extends React.PureComponent<Props> {
+ render() {
+ const { isCrossFile, locations, uniqueKey, selectedLocationIndex } = this.props;
+
+ if (!locations || locations.length === 0 || locations.every(location => !location.msg)) {
+ return null;
+ }
+
+ if (isCrossFile) {
+ return (
+ <CrossFileLocationNavigator
+ uniqueKey={uniqueKey}
+ locations={locations}
+ onLocationSelect={this.props.onLocationSelect}
+ scroll={this.props.scroll}
+ selectedLocationIndex={selectedLocationIndex}
+ />
+ );
+ }
+ return (
+ <div className="spacer-top">
+ {locations.map((location, index) => (
+ <SingleFileLocationNavigator
+ index={index}
+ // eslint-disable-next-line react/no-array-index-key
+ key={index}
+ message={location.msg}
+ onClick={this.props.onLocationSelect}
+ scroll={this.props.scroll}
+ selected={index === selectedLocationIndex}
+ />
+ ))}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.css b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.css
new file mode 100644
index 00000000000..f3e60e4a969
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.css
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+.locations-navigator {
+ position: relative;
+ z-index: var(--aboveNormalZIndex);
+ display: inline-flex;
+ align-items: flex-start;
+ max-width: 100%;
+ border: none;
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.tsx b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx
index de086bf2a10..07d329ed224 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.tsx
+++ b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx
@@ -18,8 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import LocationIndex from '../../../components/common/LocationIndex';
-import LocationMessage from '../../../components/common/LocationMessage';
+import LocationIndex from '../common/LocationIndex';
+import LocationMessage from '../common/LocationMessage';
+import './SingleFileLocationNavigator.css';
interface Props {
index: number;
@@ -29,7 +30,7 @@ interface Props {
selected: boolean;
}
-export default class ConciseIssueLocationsNavigatorLocation extends React.PureComponent<Props> {
+export default class SingleFileLocationNavigator extends React.PureComponent<Props> {
node?: HTMLElement | null;
componentDidMount() {
@@ -54,10 +55,7 @@ export default class ConciseIssueLocationsNavigatorLocation extends React.PureCo
return (
<div className="little-spacer-top" ref={node => (this.node = node)}>
- <a
- className="concise-issue-locations-navigator-location"
- href="#"
- onClick={this.handleClick}>
+ <a className="locations-navigator" href="#" onClick={this.handleClick}>
<LocationIndex selected={selected}>{index + 1}</LocationIndex>
<LocationMessage selected={selected}>{message}</LocationMessage>
</a>
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/CrossFileLocationsNavigator-test.tsx b/server/sonar-web/src/main/js/components/locations/__tests__/CrossFileLocationsNavigator-test.tsx
index a5e96b1f6d5..a5f7cd136d3 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/CrossFileLocationsNavigator-test.tsx
+++ b/server/sonar-web/src/main/js/components/locations/__tests__/CrossFileLocationsNavigator-test.tsx
@@ -19,10 +19,10 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockFlowLocation } from '../../../../helpers/testMocks';
-import { click } from '../../../../helpers/testUtils';
-import { FlowLocation } from '../../../../types/types';
-import CrossFileLocationsNavigator from '../CrossFileLocationsNavigator';
+import { click } from '../../../helpers/testUtils';
+import { mockFlowLocation } from '../../../helpers/testMocks';
+import { FlowLocation } from '../../../types/types';
+import CrossFileLocationsNavigator from '../CrossFileLocationNavigator';
const location1: FlowLocation = {
component: 'foo',
@@ -57,40 +57,40 @@ it('should render', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(2);
+ expect(wrapper.find('SingleFileLocationNavigator').length).toBe(2);
- click(wrapper.find('.concise-issue-location-file-more'));
- expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(3);
+ click(wrapper.find('.location-file-more'));
+ expect(wrapper.find('SingleFileLocationNavigator').length).toBe(3);
});
it('should render all locations', () => {
const wrapper = shallowRender({ locations: [location1, location2] });
- expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(2);
+ expect(wrapper.find('SingleFileLocationNavigator').length).toBe(2);
});
it('should expand all locations', () => {
const wrapper = shallowRender();
- expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(2);
+ expect(wrapper.find('SingleFileLocationNavigator').length).toBe(2);
wrapper.setProps({ selectedLocationIndex: 1 });
- expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(3);
+ expect(wrapper.find('SingleFileLocationNavigator').length).toBe(3);
});
it('should collapse locations when issue changes', () => {
const wrapper = shallowRender();
wrapper.setProps({ selectedLocationIndex: 1 });
- expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(3);
+ expect(wrapper.find('SingleFileLocationNavigator').length).toBe(3);
- wrapper.setProps({ issue: { key: 'def', type: 'BUG' }, selectedLocationIndex: undefined });
- expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(2);
+ wrapper.setProps({ uniqueKey: 'def', selectedLocationIndex: undefined });
+ expect(wrapper.find('SingleFileLocationNavigator').length).toBe(2);
});
function shallowRender(props: Partial<CrossFileLocationsNavigator['props']> = {}) {
return shallow<CrossFileLocationsNavigator>(
<CrossFileLocationsNavigator
- issue={{ key: 'abcd', type: 'BUG' }}
+ uniqueKey="abcd"
locations={[location1, location2, location3]}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx b/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx
new file mode 100644
index 00000000000..48b9868be13
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 { mockIssue } from '../../../helpers/testMocks';
+import { FlowLocation } from '../../../types/types';
+import LocationsList from '../LocationsList';
+
+const location1: FlowLocation = {
+ component: 'foo',
+ componentName: 'src/foo.js',
+ msg: 'Do not use foo',
+ textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 }
+};
+
+const location2: FlowLocation = {
+ component: 'foo',
+ componentName: 'src/foo.js',
+ msg: 'Do not use foo',
+ textRange: { startLine: 8, endLine: 8, startOffset: 0, endOffset: 5 }
+};
+
+const location3: FlowLocation = {
+ component: 'bar',
+ componentName: 'src/bar.js',
+ msg: 'Do not use bar',
+ textRange: { startLine: 15, endLine: 16, startOffset: 4, endOffset: 6 }
+};
+
+it('should render locations in the same file', () => {
+ const locations = [location1, location2];
+ expect(shallowRender({ uniqueKey: '', locations, isCrossFile: false })).toMatchSnapshot();
+});
+
+it('should render flow locations in different file', () => {
+ const locations = [location1, location3];
+ expect(shallowRender({ uniqueKey: '', locations, isCrossFile: true })).toMatchSnapshot();
+});
+
+it('should not render locations', () => {
+ const wrapper = shallowRender({ uniqueKey: '', locations: [] });
+ expect(wrapper.type()).toBeNull();
+});
+
+function shallowRender(overrides: Partial<LocationsList['props']> = {}) {
+ return shallow<LocationsList>(
+ <LocationsList
+ uniqueKey={mockIssue().key}
+ locations={mockIssue().secondaryLocations}
+ isCrossFile={true}
+ onLocationSelect={jest.fn()}
+ scroll={jest.fn()}
+ selectedFlowIndex={undefined}
+ selectedLocationIndex={undefined}
+ {...overrides}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationsNavigatorLocation-test.tsx b/server/sonar-web/src/main/js/components/locations/__tests__/SingleFileLocationsNavigator-test.tsx
index 53e58b9c745..9410e6ac13e 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationsNavigatorLocation-test.tsx
+++ b/server/sonar-web/src/main/js/components/locations/__tests__/SingleFileLocationsNavigator-test.tsx
@@ -19,16 +19,16 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import ConciseIssueLocationsNavigatorLocation from '../ConciseIssueLocationsNavigatorLocation';
+import SingleFileLocationNavigator from '../SingleFileLocationNavigator';
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('index 1');
expect(shallowRender({ index: 1 })).toMatchSnapshot('index 2');
});
-function shallowRender(props: Partial<ConciseIssueLocationsNavigatorLocation['props']> = {}) {
+function shallowRender(props: Partial<SingleFileLocationNavigator['props']> = {}) {
return shallow(
- <ConciseIssueLocationsNavigatorLocation
+ <SingleFileLocationNavigator
index={0}
message=""
onClick={jest.fn()}
diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap
new file mode 100644
index 00000000000..53ee06d1024
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap
@@ -0,0 +1,112 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="spacer-top"
+>
+ <div
+ className="locations-navigator-file"
+ key="0"
+ >
+ <div
+ className="location-file"
+ >
+ <i
+ className="location-file-circle little-spacer-right"
+ />
+ src/foo.js
+ </div>
+ <div
+ className="location-file-locations"
+ >
+ <SingleFileLocationNavigator
+ index={0}
+ key="0"
+ message="Do not use foo"
+ onClick={[MockFunction]}
+ scroll={[MockFunction]}
+ selected={false}
+ />
+ </div>
+ </div>
+ <div
+ className="locations-navigator-file"
+ >
+ <div
+ className="location-file"
+ >
+ <i
+ className="location-file-circle-multiple little-spacer-right"
+ />
+ <a
+ className="location-file-more"
+ href="#"
+ onClick={[Function]}
+ >
+ issues.x_more_locations.1
+ </a>
+ </div>
+ </div>
+ <div
+ className="locations-navigator-file"
+ key="1"
+ >
+ <div
+ className="location-file"
+ >
+ <i
+ className="location-file-circle little-spacer-right"
+ />
+ src/bar.js
+ </div>
+ <div
+ className="location-file-locations"
+ >
+ <SingleFileLocationNavigator
+ index={2}
+ key="2"
+ message="Do not use bar"
+ onClick={[MockFunction]}
+ scroll={[MockFunction]}
+ selected={false}
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render locations with no component name 1`] = `
+<div
+ className="spacer-top"
+>
+ <div
+ className="locations-navigator-file"
+ key="0"
+ >
+ <div
+ className="location-file"
+ >
+ <i
+ className="location-file-circle little-spacer-right"
+ />
+ </div>
+ <div
+ className="location-file-locations"
+ >
+ <SingleFileLocationNavigator
+ index={0}
+ key="0"
+ onClick={[MockFunction]}
+ scroll={[MockFunction]}
+ selected={false}
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render with no locations 1`] = `
+<div
+ className="spacer-top"
+/>
+`;
diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap
new file mode 100644
index 00000000000..9cb465ba244
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render flow locations in different file 1`] = `
+<CrossFileLocationNavigator
+ locations={
+ Array [
+ Object {
+ "component": "foo",
+ "componentName": "src/foo.js",
+ "msg": "Do not use foo",
+ "textRange": Object {
+ "endLine": 7,
+ "endOffset": 8,
+ "startLine": 7,
+ "startOffset": 5,
+ },
+ },
+ Object {
+ "component": "bar",
+ "componentName": "src/bar.js",
+ "msg": "Do not use bar",
+ "textRange": Object {
+ "endLine": 16,
+ "endOffset": 6,
+ "startLine": 15,
+ "startOffset": 4,
+ },
+ },
+ ]
+ }
+ onLocationSelect={[MockFunction]}
+ scroll={[MockFunction]}
+ uniqueKey=""
+/>
+`;
+
+exports[`should render locations in the same file 1`] = `
+<div
+ className="spacer-top"
+>
+ <SingleFileLocationNavigator
+ index={0}
+ key="0"
+ message="Do not use foo"
+ onClick={[MockFunction]}
+ scroll={[MockFunction]}
+ selected={false}
+ />
+ <SingleFileLocationNavigator
+ index={1}
+ key="1"
+ message="Do not use foo"
+ onClick={[MockFunction]}
+ scroll={[MockFunction]}
+ selected={false}
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationsNavigatorLocation-test.tsx.snap b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap
index a609334626a..46f03290c53 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationsNavigatorLocation-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap
@@ -5,7 +5,7 @@ exports[`should render correctly: index 1 1`] = `
className="little-spacer-top"
>
<a
- className="concise-issue-locations-navigator-location"
+ className="locations-navigator"
href="#"
onClick={[Function]}
>
@@ -26,7 +26,7 @@ exports[`should render correctly: index 2 1`] = `
className="little-spacer-top"
>
<a
- className="concise-issue-locations-navigator-location"
+ className="locations-navigator"
href="#"
onClick={[Function]}
>
diff --git a/server/sonar-web/src/main/js/types/security-hotspots.ts b/server/sonar-web/src/main/js/types/security-hotspots.ts
index 5d0ec2621ea..ecd85abcda1 100644
--- a/server/sonar-web/src/main/js/types/security-hotspots.ts
+++ b/server/sonar-web/src/main/js/types/security-hotspots.ts
@@ -70,6 +70,9 @@ export interface RawHotspot {
subProject?: string;
updateDate: string;
vulnerabilityProbability: RiskExposure;
+ flows?: Array<{
+ locations?: Array<Omit<FlowLocation, 'componentName'>>;
+ }>;
}
export interface Hotspot {