aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2020-11-30 18:03:18 +0100
committersonartech <sonartech@sonarsource.com>2020-12-03 20:06:38 +0000
commit9261d0a1d167022244856dd7949b57fa1f0b68f5 (patch)
tree4af5f42043dfca606c2577e915071f3b4d1c0a24 /server/sonar-web
parent634999d82271564916d7ff290c2da868ffd3d209 (diff)
downloadsonarqube-9261d0a1d167022244856dd7949b57fa1f0b68f5.tar.gz
sonarqube-9261d0a1d167022244856dd7949b57fa1f0b68f5.zip
SONAR-14120 Display file-level issues with multi-locations
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts13
6 files changed, 74 insertions, 13 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
index 704fe4fdde3..5a0d3207965 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
@@ -797,7 +797,8 @@ export default class App extends React.PureComponent<Props, State> {
handleIssueChange = (issue: T.Issue) => {
this.refreshBranchStatus();
this.setState(state => ({
- issues: state.issues.map(candidate => (candidate.key === issue.key ? issue : candidate))
+ issues: state.issues.map(candidate => (candidate.key === issue.key ? issue : candidate)),
+ openIssue: state.openIssue && state.openIssue.key === issue.key ? issue : state.openIssue
}));
};
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
index 5cb0f444f46..7f639106c24 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
@@ -353,7 +353,7 @@ it('should refresh branch status if issues are updated', async () => {
const updatedIssue: T.Issue = { ...ISSUES[0], type: 'SECURITY_HOTSPOT' };
instance.handleIssueChange(updatedIssue);
- expect(wrapper.state('issues')).toEqual([updatedIssue, ISSUES[1], ISSUES[2], ISSUES[3]]);
+ expect(wrapper.state().issues).toEqual([updatedIssue, ISSUES[1], ISSUES[2], ISSUES[3]]);
expect(fetchBranchStatus).toBeCalledWith(branchLike, component.key);
fetchBranchStatus.mockClear();
@@ -365,6 +365,18 @@ it('should refresh branch status if issues are updated', async () => {
expect(fetchBranchStatus).toBeCalled();
});
+it('should update the open issue when it is changed', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.setState({ openIssue: ISSUES[0] });
+
+ const updatedIssue: T.Issue = { ...ISSUES[0], type: 'SECURITY_HOTSPOT' };
+ wrapper.instance().handleIssueChange(updatedIssue);
+
+ expect(wrapper.state().openIssue).toBe(updatedIssue);
+});
+
it('should handle createAfter query param with time', async () => {
const fetchIssues = fetchIssuesMockFactory();
const wrapper = shallowRender({
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
index 2cc7de55fe9..c70d98dd813 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
@@ -17,8 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { noop } from 'lodash';
import * as React from 'react';
import { getSources } from '../../../api/components';
+import Issue from '../../../components/issue/Issue';
import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
@@ -344,10 +346,19 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
}
render() {
- const { branchLike, issue, issuesByLine, lastSnippetGroup, snippetGroup } = this.props;
+ const {
+ branchLike,
+ issue,
+ issuesByLine,
+ issuePopup,
+ lastSnippetGroup,
+ snippetGroup
+ } = this.props;
const { additionalLines, loading, snippets } = this.state;
const locations =
- issue.component === snippetGroup.component.key ? locationsByLine([issue]) : {};
+ issue.component === snippetGroup.component.key && issue.textRange !== undefined
+ ? locationsByLine([issue])
+ : {};
const fullyShown =
snippets.length === 1 &&
@@ -373,6 +384,18 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
onExpand={this.expandComponent}
sourceViewerFile={snippetGroup.component}
/>
+ {issue.component === snippetGroup.component.key && issue.textRange === undefined && (
+ <div className="padded-top padded-left padded-right">
+ <Issue
+ issue={issue}
+ onChange={this.props.onIssueChange}
+ onClick={noop}
+ onPopupToggle={this.props.onIssuePopupToggle}
+ openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined}
+ selected={true}
+ />
+ </div>
+ )}
{snippetLines.map((snippet, index) => (
<div id={`snippet-wrapper-${snippets[index].index}`} key={snippets[index].index}>
{this.renderSnippet({
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
index 6132a637b9f..7cbb22843f9 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
@@ -22,6 +22,7 @@ import { range, times } from 'lodash';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getSources } from '../../../../api/components';
+import Issue from '../../../../components/issue/Issue';
import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
import {
mockFlowLocation,
@@ -105,6 +106,29 @@ it('should render correctly with flows', () => {
expect(wrapper.state('snippets')[1]).toEqual({ index: 1, start: 69, end: 79 });
});
+it('should render file-level issue correctly', () => {
+ // issue with secondary locations and no primary location
+ const issue = mockIssue(true, {
+ flows: [],
+ textRange: undefined
+ });
+
+ const wrapper = shallowRender({
+ issue,
+ snippetGroup: {
+ locations: [
+ mockFlowLocation({
+ component: issue.component,
+ textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
+ })
+ ],
+ ...mockSnippetsByComponent(issue.component, range(29, 39))
+ }
+ });
+
+ expect(wrapper.find(Issue).exists()).toBe(true);
+});
+
it('should expand block', async () => {
(getSources as jest.Mock).mockResolvedValueOnce(
Object.values(mockSnippetsByComponent('a', range(6, 59)).sources)
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts
index 9dcb749ea7f..6de0bb32650 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts
@@ -176,7 +176,7 @@ describe('createSnippets', () => {
expect(results[1]).toEqual({ index: 2, start: 37, end: 47 });
});
- it('should work for location with no textrange', () => {
+ it('should ignore location with no textrange', () => {
const locations = [
mockFlowLocation({
textRange: { startLine: 85, startOffset: 2, endLine: 85, endOffset: 3 }
@@ -192,8 +192,8 @@ describe('createSnippets', () => {
issue
});
- expect(results).toHaveLength(2);
- expect(results[0]).toEqual({ index: 0, start: 1, end: 9 });
+ expect(results).toHaveLength(1);
+ expect(results[0]).toEqual({ index: 0, start: 80, end: 94 });
});
});
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts
index 94e08e78d8f..a7b0c31d28c 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts
@@ -73,7 +73,8 @@ export function createSnippets(params: {
const { component, issue, locations } = params;
const hasSecondaryLocations = issue.secondaryLocations.length > 0;
- const addIssueLocation = hasSecondaryLocations && issue.component === component;
+ const addIssueLocation =
+ hasSecondaryLocations && issue.component === component && issue.textRange !== undefined;
// For each location: compute its range, and then compare with other ranges
// to merge snippets that collide.
@@ -144,25 +145,25 @@ export function groupLocationsByComponent(
let currentGroup: T.SnippetGroup;
const groups: T.SnippetGroup[] = [];
- const addGroup = (loc: T.FlowLocation) => {
+ const addGroup = (componentKey: string) => {
currentGroup = {
- ...(components[loc.component] || unknownComponent(loc.component)),
+ ...(components[componentKey] || unknownComponent(componentKey)),
locations: []
};
groups.push(currentGroup);
- currentComponent = loc.component;
+ currentComponent = componentKey;
};
if (
issue.secondaryLocations.length > 0 &&
locations.every(loc => loc.component !== issue.component)
) {
- addGroup(getPrimaryLocation(issue));
+ addGroup(issue.component);
}
locations.forEach((loc, index) => {
if (loc.component !== currentComponent) {
- addGroup(loc);
+ addGroup(loc.component);
}
loc.index = index;
currentGroup.locations.push(loc);