diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2020-11-30 18:03:18 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-12-03 20:06:38 +0000 |
commit | 9261d0a1d167022244856dd7949b57fa1f0b68f5 (patch) | |
tree | 4af5f42043dfca606c2577e915071f3b4d1c0a24 /server/sonar-web | |
parent | 634999d82271564916d7ff290c2da868ffd3d209 (diff) | |
download | sonarqube-9261d0a1d167022244856dd7949b57fa1f0b68f5.tar.gz sonarqube-9261d0a1d167022244856dd7949b57fa1f0b68f5.zip |
SONAR-14120 Display file-level issues with multi-locations
Diffstat (limited to 'server/sonar-web')
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); |