diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2019-05-02 17:50:21 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-05-07 20:21:26 +0200 |
commit | f2f3ede233eca98118c026c7cf151145eb0dfa99 (patch) | |
tree | 536223c3b3ad7df40de1bb4483a41a9e7aa09d02 | |
parent | 0e66ef03f6d1a86fa12f7b9574f6e9f9b36aa3a2 (diff) | |
download | sonarqube-f2f3ede233eca98118c026c7cf151145eb0dfa99.tar.gz sonarqube-f2f3ede233eca98118c026c7cf151145eb0dfa99.zip |
SONAR-12076 Add duplication popup back to the new multi location issue flow
21 files changed, 788 insertions, 126 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 7b97f66111c..6141c5d879b 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -276,8 +276,10 @@ export function getSources( return getJSON('/api/sources/lines', data).then(r => r.sources); } -export function getDuplications(data: { key: string } & T.BranchParameters): Promise<any> { - return getJSON('/api/duplications/show', data); +export function getDuplications( + data: { key: string } & T.BranchParameters +): Promise<{ duplications: T.Duplication[]; files: T.Dict<T.DuplicatedFile> }> { + return getJSON('/api/duplications/show', data).catch(throwGlobalError); } export function getTests( diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts index 5ec635aab23..27fde4e2538 100644 --- a/server/sonar-web/src/main/js/app/types.d.ts +++ b/server/sonar-web/src/main/js/app/types.d.ts @@ -257,7 +257,7 @@ declare namespace T { } export interface DuplicationBlock { - _ref: string; + _ref?: string; from: number; size: number; } @@ -419,6 +419,13 @@ declare namespace T { [line: number]: SourceLine; } + export interface LinePopup { + index?: number; + line: number; + name: string; + open?: boolean; + } + export interface LoggedInUser extends CurrentUser { avatar?: string; email?: string; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx index 3e362317db0..d989e9f5a4a 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import { createSnippets, expandSnippet, @@ -26,11 +27,11 @@ import { LINES_BELOW_LAST, MERGE_DISTANCE } from './utils'; -import { getSources } from '../../../api/components'; import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon'; import Line from '../../../components/SourceViewer/components/Line'; import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim'; import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus'; +import { getSources } from '../../../api/components'; import { symbolsByLine, locationsByLine } from '../../../components/SourceViewer/helpers/indexing'; import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations'; import { @@ -42,16 +43,25 @@ import { translate } from '../../../helpers/l10n'; interface Props { branchLike: T.BranchLike | undefined; + duplications?: T.Duplication[]; + duplicationsByLine?: { [line: number]: number[] }; highlightedLocationMessage: { index: number; text: string | undefined } | undefined; issue: T.Issue; issuePopup?: { issue: string; name: string }; issuesByLine: T.IssuesByLine; last: boolean; + linePopup?: T.LinePopup; + loadDuplications: (component: string, line: T.SourceLine) => void; locations: T.FlowLocation[]; onIssueChange: (issue: T.Issue) => void; onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; + onLinePopupToggle: (linePopup: T.LinePopup & { component: string }) => void; onLocationSelect: (index: number) => void; - renderDuplicationPopup: (index: number, line: number) => JSX.Element; + renderDuplicationPopup: ( + component: T.SourceViewerFile, + index: number, + line: number + ) => React.ReactNode; scroll?: (element: HTMLElement) => void; snippetGroup: T.SnippetGroup; } @@ -59,7 +69,6 @@ interface Props { interface State { additionalLines: { [line: number]: T.SourceLine }; highlightedSymbols: string[]; - linePopup?: { index?: number; line: number; name: string }; loading: boolean; openIssuesByLine: T.Dict<boolean>; snippets: T.SourceLine[][]; @@ -140,7 +149,6 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr return { additionalLines: combinedLines, - linePopup: undefined, snippets: expandSnippet({ direction, lines: { ...combinedLines, ...this.props.snippetGroup.sources }, @@ -163,7 +171,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr getSources({ key }).then( lines => { if (this.mounted) { - this.setState({ linePopup: undefined, loading: false, snippets: [lines] }); + this.setState({ loading: false, snippets: [lines] }); } }, () => { @@ -174,29 +182,10 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr ); }; - handleLinePopupToggle = ({ - index, - line, - name, - open - }: { - index?: number; - line: number; - name: string; - open?: boolean; - }) => { - this.setState((state: State) => { - const samePopup = - state.linePopup !== undefined && - state.linePopup.name === name && - state.linePopup.line === line && - state.linePopup.index === index; - if (open !== false && !samePopup) { - return { linePopup: { index, line, name } }; - } else if (open !== true && samePopup) { - return { linePopup: undefined }; - } - return null; + handleLinePopupToggle = (linePopup: T.LinePopup) => { + this.props.onLinePopupToggle({ + ...linePopup, + component: this.props.snippetGroup.component.key }); }; @@ -216,6 +205,14 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr this.setState({ highlightedSymbols }); }; + loadDuplications = (line: T.SourceLine) => { + this.props.loadDuplications(this.props.snippetGroup.component.key, line); + }; + + renderDuplicationPopup = (index: number, line: number) => { + return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line); + }; + renderLine({ index, issuesForLine, @@ -234,10 +231,12 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr verticalBuffer: number; }) { const { openIssuesByLine } = this.state; - const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations); - const noop = () => {}; + const { duplications, duplicationsByLine } = this.props; + const duplicationsCount = duplications ? duplications.length : 0; + const lineDuplications = + (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || []; const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key); @@ -246,11 +245,11 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr branchLike={undefined} displayAllIssues={false} displayCoverage={true} - displayDuplications={false} + displayDuplications={!!line.duplicated} displayIssues={!isSinkLine || issuesForLine.length > 1} displayLocationMarkers={true} - duplications={[]} - duplicationsCount={0} + duplications={lineDuplications} + duplicationsCount={duplicationsCount} highlighted={false} highlightedLocationMessage={optimizeLocationMessage( this.props.highlightedLocationMessage, @@ -263,12 +262,12 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr key={line.line} last={false} line={line} - linePopup={this.state.linePopup} - loadDuplications={noop} + linePopup={this.props.linePopup} + loadDuplications={this.loadDuplications} onIssueChange={this.props.onIssueChange} onIssuePopupToggle={this.props.onIssuePopupToggle} - onIssueSelect={noop} - onIssueUnselect={noop} + onIssueSelect={() => {}} + onIssueUnselect={() => {}} onIssuesClose={this.handleCloseIssues} onIssuesOpen={this.handleOpenIssues} onLinePopupToggle={this.handleLinePopupToggle} @@ -276,7 +275,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr onSymbolClick={this.handleSymbolClick} openIssues={openIssuesByLine[line.line]} previousLine={index > 0 ? snippet[index - 1] : undefined} - renderDuplicationPopup={this.props.renderDuplicationPopup} + renderDuplicationPopup={this.renderDuplicationPopup} scroll={this.props.scroll} secondaryIssueLocations={secondaryIssueLocations} selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)} @@ -359,7 +358,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr } render() { - const { branchLike, issue, issuesByLine, last, snippetGroup } = this.props; + const { branchLike, duplications, issue, issuesByLine, last, snippetGroup } = this.props; const { loading, snippets } = this.state; const locations = locationsByLine([issue]); @@ -369,7 +368,10 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr snippets[0].length === parseInt(snippetGroup.component.measures.lines || '', 10); return ( - <div className="component-source-container"> + <div + className={classNames('component-source-container', { + 'source-duplications-expanded': duplications && duplications.length > 0 + })}> <SourceViewerHeaderSlim branchLike={branchLike} expandable={!fullyShown} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx index 92760bb3b22..8cdcbbdfd62 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx @@ -21,14 +21,20 @@ import * as React from 'react'; import ComponentSourceSnippetViewer from './ComponentSourceSnippetViewer'; import { groupLocationsByComponent } from './utils'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup'; +import { WorkspaceContext } from '../../../components/workspace/context'; import { getIssueFlowSnippets } from '../../../api/issues'; -import { issuesByComponentAndLine } from '../../../components/SourceViewer/helpers/indexing'; - -interface State { - components: T.Dict<T.SnippetsByComponent>; - issuePopup?: { issue: string; name: string }; - loading: boolean; -} +import { + filterDuplicationBlocksByLine, + isDuplicationBlockInRemovedComponent, + getDuplicationBlocksForIndex +} from '../../../components/SourceViewer/helpers/duplications'; +import { + duplicationsByLine, + issuesByComponentAndLine +} from '../../../components/SourceViewer/helpers/indexing'; +import { getDuplications } from '../../../api/components'; +import { getBranchLikeQuery } from '../../../helpers/branches'; interface Props { branchLike: T.Branch | T.PullRequest | undefined; @@ -39,15 +45,25 @@ interface Props { onIssueChange: (issue: T.Issue) => void; onLoaded?: () => void; onLocationSelect: (index: number) => void; - renderDuplicationPopup: (index: number, line: number) => JSX.Element; scroll?: (element: HTMLElement) => void; selectedFlowIndex: number | undefined; } +interface State { + components: T.Dict<T.SnippetsByComponent>; + duplicatedFiles?: T.Dict<T.DuplicatedFile>; + duplications?: T.Duplication[]; + duplicationsByLine: { [line: number]: number[] }; + issuePopup?: { issue: string; name: string }; + linePopup?: T.LinePopup & { component: string }; + loading: boolean; +} + export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> { mounted = false; state: State = { components: {}, + duplicationsByLine: {}, loading: true }; @@ -66,12 +82,39 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone this.mounted = false; } + fetchDuplications = (component: string, line: T.SourceLine) => { + getDuplications({ + key: component, + ...getBranchLikeQuery(this.props.branchLike) + }).then( + r => { + if (this.mounted) { + this.setState(state => ({ + duplicatedFiles: r.files, + duplications: r.duplications, + duplicationsByLine: duplicationsByLine(r.duplications), + linePopup: + r.duplications.length === 1 + ? { component, index: 0, line: line.line, name: 'duplications' } + : state.linePopup + })); + } + }, + () => {} + ); + }; + fetchIssueFlowSnippets(issueKey: string) { this.setState({ loading: true }); getIssueFlowSnippets(issueKey).then( components => { if (this.mounted) { - this.setState({ components, issuePopup: undefined, loading: false }); + this.setState({ + components, + issuePopup: undefined, + linePopup: undefined, + loading: false + }); if (this.props.onLoaded) { this.props.onLoaded(); } @@ -98,8 +141,61 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone }); }; + handleLinePopupToggle = ({ + component, + index, + line, + name, + open + }: T.LinePopup & { component: string }) => { + this.setState((state: State) => { + const samePopup = + state.linePopup !== undefined && + state.linePopup.line === line && + state.linePopup.name === name && + state.linePopup.component === component && + state.linePopup.index === index; + if (open !== false && !samePopup) { + return { linePopup: { component, index, line, name } }; + } else if (open !== true && samePopup) { + return { linePopup: undefined }; + } + return null; + }); + }; + + handleCloseLinePopup = () => { + this.setState({ linePopup: undefined }); + }; + + renderDuplicationPopup = (component: T.SourceViewerFile, index: number, line: number) => { + const { duplicatedFiles, duplications } = this.state; + + if (!component || !duplicatedFiles) { + return null; + } + + const blocks = getDuplicationBlocksForIndex(duplications, index); + + return ( + <WorkspaceContext.Consumer> + {({ openComponent }) => ( + <DuplicationPopup + blocks={filterDuplicationBlocksByLine(blocks, line)} + branchLike={this.props.branchLike} + duplicatedFiles={duplicatedFiles} + inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)} + onClose={this.handleCloseLinePopup} + openComponent={openComponent} + sourceViewerFile={component} + /> + )} + </WorkspaceContext.Consumer> + ); + }; + render() { - const { components, loading } = this.state; + const { loading } = this.state; if (loading) { return ( @@ -109,29 +205,43 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone ); } + const { components, duplications, duplicationsByLine, linePopup } = this.state; const issuesByComponent = issuesByComponentAndLine(this.props.issues); const locationsByComponent = groupLocationsByComponent(this.props.locations, components); return ( <div> - {locationsByComponent.map((g, i) => ( - <ComponentSourceSnippetViewer - branchLike={this.props.branchLike} - highlightedLocationMessage={this.props.highlightedLocationMessage} - issue={this.props.issue} - issuePopup={this.state.issuePopup} - issuesByLine={issuesByComponent[g.component.key] || {}} - key={`${this.props.issue.key}-${this.props.selectedFlowIndex}-${i}`} - last={i === locationsByComponent.length - 1} - locations={g.locations || []} - onIssueChange={this.props.onIssueChange} - onIssuePopupToggle={this.handleIssuePopupToggle} - onLocationSelect={this.props.onLocationSelect} - renderDuplicationPopup={this.props.renderDuplicationPopup} - scroll={this.props.scroll} - snippetGroup={g} - /> - ))} + {locationsByComponent.map((snippetGroup, i) => { + let componentProps = {}; + if (linePopup && snippetGroup.component.key === linePopup.component) { + componentProps = { + duplications, + duplicationsByLine, + linePopup: { index: linePopup.index, line: linePopup.line, name: linePopup.name } + }; + } + return ( + <ComponentSourceSnippetViewer + branchLike={this.props.branchLike} + highlightedLocationMessage={this.props.highlightedLocationMessage} + issue={this.props.issue} + issuePopup={this.state.issuePopup} + issuesByLine={issuesByComponent[snippetGroup.component.key] || {}} + key={`${this.props.issue.key}-${this.props.selectedFlowIndex}-${i}`} + last={i === locationsByComponent.length - 1} + loadDuplications={this.fetchDuplications} + locations={snippetGroup.locations || []} + onIssueChange={this.props.onIssueChange} + onIssuePopupToggle={this.handleIssuePopupToggle} + onLinePopupToggle={this.handleLinePopupToggle} + onLocationSelect={this.props.onLocationSelect} + renderDuplicationPopup={this.renderDuplicationPopup} + scroll={this.props.scroll} + snippetGroup={snippetGroup} + {...componentProps} + /> + ); + })} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx index 980df641334..6b194e14452 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx @@ -118,6 +118,55 @@ it('should handle symbol highlighting', () => { expect(wrapper.state('highlightedSymbols')).toEqual(['foo']); }); +it('should correctly handle lines actions', () => { + const snippetGroup: T.SnippetGroup = { + locations: [ + mockFlowLocation({ + component: 'a', + textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 } + }), + mockFlowLocation({ + component: 'a', + textRange: { startLine: 54, endLine: 54, startOffset: 0, endOffset: 0 } + }) + ], + ...mockSnippetsByComponent('a', [32, 33, 34, 35, 36, 52, 53, 54, 55, 56]) + }; + const loadDuplications = jest.fn(); + const onLinePopupToggle = jest.fn(); + const renderDuplicationPopup = jest.fn(); + + const wrapper = shallowRender({ + loadDuplications, + onLinePopupToggle, + renderDuplicationPopup, + snippetGroup + }); + + const line = mockSourceLine(); + wrapper + .find('Line') + .first() + .prop<Function>('loadDuplications')(line); + expect(loadDuplications).toHaveBeenCalledWith('a', line); + + wrapper + .find('Line') + .first() + .prop<Function>('onLinePopupToggle')({ line: 13, name: 'foo' }); + expect(onLinePopupToggle).toHaveBeenCalledWith({ component: 'a', line: 13, name: 'foo' }); + + wrapper + .find('Line') + .first() + .prop<Function>('renderDuplicationPopup')(1, 13); + expect(renderDuplicationPopup).toHaveBeenCalledWith( + mockSourceViewerFile({ key: 'a', path: 'a' }), + 1, + 13 + ); +}); + function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = {}) { const snippetGroup: T.SnippetGroup = { component: mockSourceViewerFile(), @@ -127,13 +176,18 @@ function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = { return shallow<ComponentSourceSnippetViewer>( <ComponentSourceSnippetViewer branchLike={mockMainBranch()} + duplications={undefined} + duplicationsByLine={undefined} highlightedLocationMessage={{ index: 0, text: '' }} issue={mockIssue()} issuesByLine={{}} last={false} + linePopup={undefined} + loadDuplications={jest.fn()} locations={[]} onIssueChange={jest.fn()} onIssuePopupToggle={jest.fn()} + onLinePopupToggle={jest.fn()} onLocationSelect={jest.fn()} renderDuplicationPopup={jest.fn()} scroll={jest.fn()} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx index 634537fd178..4d283b416d1 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx @@ -20,31 +20,46 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import CrossComponentSourceViewerWrapper from '../CrossComponentSourceViewerWrapper'; -import { mockIssue, mockSourceViewerFile } from '../../../../helpers/testMocks'; +import { + mockFlowLocation, + mockIssue, + mockSnippetsByComponent, + mockSourceLine, + mockSourceViewerFile +} from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import { getIssueFlowSnippets } from '../../../../api/issues'; +import { getDuplications } from '../../../../api/components'; jest.mock('../../../../api/issues', () => { - const { mockSourceViewerFile } = require.requireActual('../../../../helpers/testMocks'); + const { mockSnippetsByComponent } = require.requireActual('../../../../helpers/testMocks'); return { - getIssueFlowSnippets: jest.fn().mockResolvedValue([mockSourceViewerFile()]) + getIssueFlowSnippets: jest.fn().mockResolvedValue({ 'main.js': mockSnippetsByComponent() }) }; }); +jest.mock('../../../../api/components', () => ({ + getDuplications: jest.fn().mockResolvedValue({}) +})); + beforeEach(() => { jest.clearAllMocks(); }); -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); +it('should render correctly', async () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); + + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); }); it('Should fetch data', async () => { const wrapper = shallowRender(); wrapper.instance().fetchIssueFlowSnippets('124'); await waitAndUpdate(wrapper); - expect(getIssueFlowSnippets).toBeCalled(); - expect(wrapper.state('components')).toEqual([mockSourceViewerFile()]); + expect(getIssueFlowSnippets).toHaveBeenCalledWith('1'); + expect(wrapper.state('components')).toEqual({ 'main.js': mockSnippetsByComponent() }); (getIssueFlowSnippets as jest.Mock).mockClear(); wrapper.setProps({ issue: mockIssue(true, { key: 'foo' }) }); @@ -62,18 +77,68 @@ it('should handle issue popup', () => { expect(wrapper.state('issuePopup')).toBeUndefined(); }); +it('should handle line popup', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + const linePopup = { component: 'foo', index: 0, line: 16, name: 'b.tsx' }; + wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(linePopup); + expect(wrapper.state('linePopup')).toEqual(linePopup); + + wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(linePopup); + expect(wrapper.state('linePopup')).toEqual(undefined); + + const openLinePopup = { ...linePopup, open: true }; + wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(openLinePopup); + wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(openLinePopup); + expect(wrapper.state('linePopup')).toEqual(linePopup); +}); + +it('should handle duplication popup', async () => { + const files = { b: { key: 'b', name: 'B.tsx', project: 'foo', projectName: 'Foo' } }; + const duplications = [{ blocks: [{ _ref: '1', from: 1, size: 2 }] }]; + (getDuplications as jest.Mock).mockResolvedValueOnce({ duplications, files }); + + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.find('ComponentSourceSnippetViewer').prop<Function>('loadDuplications')( + 'foo', + mockSourceLine() + ); + + await waitAndUpdate(wrapper); + expect(getDuplications).toHaveBeenCalledWith({ key: 'foo' }); + expect(wrapper.state('duplicatedFiles')).toEqual(files); + expect(wrapper.state('duplications')).toEqual(duplications); + expect(wrapper.state('duplicationsByLine')).toEqual({ '1': [0], '2': [0] }); + expect(wrapper.state('linePopup')).toEqual({ + component: 'foo', + index: 0, + line: 16, + name: 'duplications' + }); + + expect( + wrapper.find('ComponentSourceSnippetViewer').prop<Function>('renderDuplicationPopup')( + mockSourceViewerFile(), + 0, + 16 + ) + ).toMatchSnapshot(); +}); + function shallowRender(props: Partial<CrossComponentSourceViewerWrapper['props']> = {}) { return shallow<CrossComponentSourceViewerWrapper>( <CrossComponentSourceViewerWrapper branchLike={undefined} highlightedLocationMessage={undefined} - issue={mockIssue(true)} + issue={mockIssue(true, { key: '1' })} issues={[]} - locations={[]} + locations={[mockFlowLocation()]} onIssueChange={jest.fn()} onLoaded={jest.fn()} onLocationSelect={jest.fn()} - renderDuplicationPopup={jest.fn()} scroll={jest.fn()} selectedFlowIndex={0} {...props} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap index 9283b3f518f..1aeb92a076d 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap @@ -1,5 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should handle duplication popup 1`] = ` +<Context.Consumer> + [Function] +</Context.Consumer> +`; + exports[`should render correctly 1`] = ` <div> <DeferredSpinner @@ -7,3 +13,179 @@ exports[`should render correctly 1`] = ` /> </div> `; + +exports[`should render correctly 2`] = ` +<div> + <ComponentSourceSnippetViewer + 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": "1", + "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 [ + 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", + } + } + issuesByLine={Object {}} + key="1-0-0" + last={true} + loadDuplications={[Function]} + locations={ + Array [ + Object { + "component": "main.js", + "index": 0, + "textRange": Object { + "endLine": 2, + "endOffset": 2, + "startLine": 1, + "startOffset": 1, + }, + }, + ] + } + onIssueChange={[MockFunction]} + onIssuePopupToggle={[Function]} + onLinePopupToggle={[Function]} + onLocationSelect={[MockFunction]} + renderDuplicationPopup={[Function]} + scroll={[MockFunction]} + snippetGroup={ + Object { + "component": Object { + "key": "main.js", + "measures": Object { + "coverage": "85.2", + "duplicationDensity": "1.0", + "issues": "12", + "lines": "56", + }, + "path": "main.js", + "project": "my-project", + "projectName": "MyProject", + "q": "FIL", + "uuid": "foo-bar", + }, + "locations": Array [ + Object { + "component": "main.js", + "index": 0, + "textRange": Object { + "endLine": 2, + "endOffset": 2, + "startLine": 1, + "startOffset": 1, + }, + }, + ], + "sources": Object { + "16": Object { + "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", + "coverageStatus": "covered", + "coveredConditions": 2, + "duplicated": false, + "isNew": true, + "line": 16, + "scmAuthor": "simon.brandhof@sonarsource.com", + "scmDate": "2018-12-11T10:48:39+0100", + "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", + }, + }, + } + } + /> +</div> +`; 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 1cb4a7e4973..85d3c2e3460 100644 --- a/server/sonar-web/src/main/js/apps/issues/styles.css +++ b/server/sonar-web/src/main/js/apps/issues/styles.css @@ -254,8 +254,11 @@ text-align: left; cursor: pointer; } -.snippet > .expand-block:hover { +.snippet > .expand-block:hover, +.snippet > .expand-block:focus, +.snippet > .expand-block:active { color: var(--darkBlue); + outline: none; } .snippet > .expand-block-above { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAADdJREFUCB1dzMEKADAIAlBd1v9/bcc2YgRjHh8qq2qTxCQzsX4wM6y30RARF3sy0Es1SIK7Y64OpCES1W69JS4AAAAASUVORK5CYII='); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx index 10341ecd3b1..3abf5f6e0d6 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx @@ -28,11 +28,17 @@ import DuplicationPopup from './components/DuplicationPopup'; import defaultLoadIssues from './helpers/loadIssues'; import getCoverageStatus from './helpers/getCoverageStatus'; import { + filterDuplicationBlocksByLine, + getDuplicationBlocksForIndex, + isDuplicationBlockInRemovedComponent +} from './helpers/duplications'; +import { duplicationsByLine, issuesByLine, locationsByLine, symbolsByLine } from './helpers/indexing'; +import { Alert } from '../ui/Alert'; import { getComponentData, getComponentForSourceViewer, @@ -41,7 +47,6 @@ import { } from '../../api/components'; import { isSameBranchLike, getBranchLikeQuery } from '../../helpers/branches'; import { translate } from '../../helpers/l10n'; -import { Alert } from '../ui/Alert'; import { WorkspaceContext } from '../workspace/context'; import './styles.css'; @@ -97,7 +102,7 @@ interface State { issuePopup?: { issue: string; name: string }; issues?: T.Issue[]; issuesByLine: { [line: number]: T.Issue[] }; - linePopup?: { index?: number; line: number; name: string }; + linePopup?: T.LinePopup; loading: boolean; loadingSourcesAfter: boolean; loadingSourcesBefore: boolean; @@ -495,17 +500,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> ); }; - handleLinePopupToggle = ({ - index, - line, - name, - open - }: { - index?: number; - line: number; - name: string; - open?: boolean; - }) => { + handleLinePopupToggle = ({ index, line, name, open }: T.LinePopup) => { this.setState((state: State) => { const samePopup = state.linePopup !== undefined && @@ -587,34 +582,20 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> renderDuplicationPopup = (index: number, line: number) => { const { component, duplicatedFiles, duplications } = this.state; - if (!component || !duplicatedFiles) return <></>; - - const duplication = duplications && duplications[index]; - let blocks = (duplication && duplication.blocks) || []; - /* eslint-disable no-underscore-dangle */ - const inRemovedComponent = blocks.some(b => b._ref === undefined); - let foundOne = false; - blocks = blocks.filter(b => { - const outOfBounds = b.from > line || b.from + b.size < line; - const currentFile = b._ref === '1'; - const shouldDisplayForCurrentFile = outOfBounds || foundOne; - const shouldDisplay = !currentFile || shouldDisplayForCurrentFile; - const isOk = b._ref !== undefined && shouldDisplay; - if (b._ref === '1' && !outOfBounds) { - foundOne = true; - } - return isOk; - }); - /* eslint-enable no-underscore-dangle */ + if (!component || !duplicatedFiles) { + return null; + } + + const blocks = getDuplicationBlocksForIndex(duplications, index); return ( <WorkspaceContext.Consumer> {({ openComponent }) => ( <DuplicationPopup - blocks={blocks} + blocks={filterDuplicationBlocksByLine(blocks, line)} branchLike={this.props.branchLike} duplicatedFiles={duplicatedFiles} - inRemovedComponent={inRemovedComponent} + inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)} onClose={this.closeLinePopup} openComponent={openComponent} sourceViewerFile={component} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx index 04ecb86b671..9afe3118d43 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx @@ -56,7 +56,7 @@ interface Props { issuePopup: { issue: string; name: string } | undefined; issues: T.Issue[] | undefined; issuesByLine: { [line: number]: T.Issue[] }; - linePopup: { index?: number; line: number; name: string } | undefined; + linePopup: T.LinePopup | undefined; loadDuplications: (line: T.SourceLine) => void; loadingSourcesAfter: boolean; loadingSourcesBefore: boolean; @@ -68,11 +68,11 @@ interface Props { onIssueSelect: (issueKey: string) => void; onIssuesOpen: (line: T.SourceLine) => void; onIssueUnselect: () => void; - onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + onLinePopupToggle: (linePopup: T.LinePopup) => void; onLocationSelect: ((index: number) => void) | undefined; onSymbolClick: (symbols: string[]) => void; openIssuesByLine: { [line: number]: boolean }; - renderDuplicationPopup: (index: number, line: number) => JSX.Element; + renderDuplicationPopup: (index: number, line: number) => React.ReactNode; scroll?: (element: HTMLElement) => void; selectedIssue: string | undefined; sources: T.SourceLine[]; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx index 79e938a542c..9dfa67cf3a0 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx @@ -46,9 +46,9 @@ interface Props { issues: T.Issue[]; last: boolean; line: T.SourceLine; - linePopup: { index?: number; line: number; name: string } | undefined; + linePopup: T.LinePopup | undefined; loadDuplications: (line: T.SourceLine) => void; - onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + onLinePopupToggle: (linePopup: T.LinePopup) => void; onIssueChange: (issue: T.Issue) => void; onIssuePopupToggle: (issueKey: string, popupName: string, open?: boolean) => void; onIssuesClose: (line: T.SourceLine) => void; @@ -59,7 +59,7 @@ interface Props { onSymbolClick: (symbols: string[]) => void; openIssues: boolean; previousLine: T.SourceLine | undefined; - renderDuplicationPopup: (index: number, line: number) => JSX.Element; + renderDuplicationPopup: (index: number, line: number) => React.ReactNode; scroll?: (element: HTMLElement) => void; secondaryIssueLocations: T.LinearIssueLocation[]; selectedIssue: string | undefined; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx index 334b97b260a..cf75f6912a3 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx @@ -27,9 +27,9 @@ interface Props { duplicated: boolean; index: number; line: T.SourceLine; - onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + onPopupToggle: (linePopup: T.LinePopup) => void; popupOpen: boolean; - renderDuplicationPopup: (index: number, line: number) => JSX.Element; + renderDuplicationPopup: (index: number, line: number) => React.ReactNode; } export default class LineDuplicationBlock extends React.PureComponent<Props> { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx index 5d90fb7acdd..137f14ba526 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx @@ -23,7 +23,7 @@ import Toggler from '../../controls/Toggler'; interface Props { line: T.SourceLine; - onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + onPopupToggle: (linePopup: T.LinePopup) => void; popupOpen: boolean; } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx index 8f549955150..151dc7422b0 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx @@ -23,7 +23,7 @@ import Toggler from '../../controls/Toggler'; interface Props { line: T.SourceLine; - onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + onPopupToggle: (linePopup: T.LinePopup) => void; popupOpen: boolean; previousLine: T.SourceLine | undefined; } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap new file mode 100644 index 00000000000..2e6635c4494 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`loadIssues should load issues 1`] = ` +Array [ + Object { + "actions": Array [ + "set_tags", + "comment", + "assign", + ], + "assignee": "luke", + "assigneeActive": true, + "assigneeAvatar": "lukavatar", + "assigneeLogin": "luke", + "assigneeName": "Luke", + "author": "luke@sonarsource.com", + "comments": Array [], + "component": "foo.java", + "componentEnabled": true, + "componentKey": "foo.java", + "componentLongName": "Foo.java", + "componentName": "foo.java", + "componentOrganization": "default-organization", + "componentPath": "/foo.java", + "componentQualifier": "FIL", + "creationDate": "2016-08-15T15:25:38+0200", + "flows": Array [], + "fromHotspot": true, + "hash": "78417dcee7ba927b7e7c9161e29e02b8", + "key": "AWaqVGl3tut9VbnJvk6M", + "line": 62, + "message": "Make sure this file handling is safe here.", + "organization": "default-organization", + "project": "org.sonarsource.java:java", + "projectEnabled": true, + "projectKey": "org.sonarsource.java:java", + "projectLongName": "SonarJava", + "projectName": "SonarJava", + "projectOrganization": "default-organization", + "projectQualifier": "TRK", + "rule": "squid:S4797", + "ruleKey": "squid:S4797", + "ruleLang": "java", + "ruleLangName": "Java", + "ruleName": "Handling files is security-sensitive", + "ruleStatus": "READY", + "secondaryLocations": Array [], + "status": "OPEN", + "tags": Array [ + "cert", + "cwe", + "owasp-a1", + "owasp-a3", + ], + "textRange": Object { + "endLine": 62, + "endOffset": 96, + "startLine": 62, + "startOffset": 93, + }, + "transitions": Array [], + "type": "SECURITY_HOTSPOT", + "updateDate": "2018-10-25T10:23:08+0200", + }, +] +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/duplications-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/duplications-test.ts new file mode 100644 index 00000000000..03512c6330d --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/duplications-test.ts @@ -0,0 +1,49 @@ +/* + * 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 { + getDuplicationBlocksForIndex, + isDuplicationBlockInRemovedComponent +} from '../duplications'; + +describe('getDuplicationBlocksForIndex', () => { + it('should return duplications blocks', () => { + const blocks = [{ _ref: '0', from: 2, size: 2 }]; + expect(getDuplicationBlocksForIndex([{ blocks }], 0)).toBe(blocks); + expect(getDuplicationBlocksForIndex([{ blocks }], 5)).toEqual([]); + expect(getDuplicationBlocksForIndex(undefined, 5)).toEqual([]); + }); +}); + +describe('isDuplicationBlockInRemovedComponent', () => { + it('should ', () => { + expect( + isDuplicationBlockInRemovedComponent([ + { _ref: '0', from: 2, size: 2 }, + { _ref: '0', from: 3, size: 1 } + ]) + ).toBe(false); + expect( + isDuplicationBlockInRemovedComponent([ + { _ref: undefined, from: 2, size: 2 }, + { _ref: '0', from: 3, size: 1 } + ]) + ).toBe(true); + }); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/loadIssues-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/loadIssues-test.ts new file mode 100644 index 00000000000..932eec9688b --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/loadIssues-test.ts @@ -0,0 +1,92 @@ +/* + * 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 loadIssues from '../loadIssues'; +import { mockMainBranch } from '../../../../helpers/testMocks'; + +jest.mock('../../../../api/issues', () => ({ + searchIssues: jest.fn().mockResolvedValue({ + paging: { pageIndex: 1, pageSize: 500, total: 1 }, + effortTotal: 15, + debtTotal: 15, + issues: [ + { + key: 'AWaqVGl3tut9VbnJvk6M', + rule: 'squid:S4797', + component: 'foo.java', + project: 'org.sonarsource.java:java', + line: 62, + hash: '78417dcee7ba927b7e7c9161e29e02b8', + textRange: { startLine: 62, endLine: 62, startOffset: 93, endOffset: 96 }, + flows: [], + status: 'OPEN', + message: 'Make sure this file handling is safe here.', + assignee: 'luke', + author: 'luke@sonarsource.com', + tags: ['cert', 'cwe', 'owasp-a1', 'owasp-a3'], + transitions: [], + actions: ['set_tags', 'comment', 'assign'], + comments: [], + creationDate: '2016-08-15T15:25:38+0200', + updateDate: '2018-10-25T10:23:08+0200', + type: 'SECURITY_HOTSPOT', + organization: 'default-organization', + fromHotspot: true + } + ], + components: [ + { + organization: 'default-organization', + key: 'org.sonarsource.java:java', + enabled: true, + qualifier: 'TRK', + name: 'SonarJava', + longName: 'SonarJava' + }, + { + organization: 'default-organization', + key: 'foo.java', + enabled: true, + qualifier: 'FIL', + name: 'foo.java', + longName: 'Foo.java', + path: '/foo.java' + } + ], + rules: [ + { + key: 'squid:S4797', + name: 'Handling files is security-sensitive', + lang: 'java', + status: 'READY', + langName: 'Java' + } + ], + users: [{ login: 'luke', name: 'Luke', avatar: 'lukavatar', active: true }], + languages: [{ key: 'java', name: 'Java' }], + facets: [] + }) +})); + +describe('loadIssues', () => { + it('should load issues', async () => { + const result = await loadIssues('foo.java', 1, 500, mockMainBranch()); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/duplications.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/duplications.ts new file mode 100644 index 00000000000..90954569c96 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/duplications.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +// TODO Test this function, but I don't get the logic behind it +export function filterDuplicationBlocksByLine(blocks: T.DuplicationBlock[], line: number) { + /* eslint-disable no-underscore-dangle */ + let foundOne = false; + return blocks.filter(b => { + const outOfBounds = b.from > line || b.from + b.size < line; + const currentFile = b._ref === '1'; + const shouldDisplayForCurrentFile = outOfBounds || foundOne; + const shouldDisplay = !currentFile || shouldDisplayForCurrentFile; + const isOk = b._ref !== undefined && shouldDisplay; + if (b._ref === '1' && !outOfBounds) { + foundOne = true; + } + return isOk; + }); + /* eslint-enable no-underscore-dangle */ +} + +export function getDuplicationBlocksForIndex( + duplications: T.Duplication[] | undefined, + index: number +) { + return (duplications && duplications[index] && duplications[index].blocks) || []; +} + +export function isDuplicationBlockInRemovedComponent(blocks: T.DuplicationBlock[]) { + return blocks.some(b => b._ref === undefined); // eslint-disable-line no-underscore-dangle +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.ts index 2315af1115d..2315af1115d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.ts diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts index 0bf83942ced..0bf83942ced 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts diff --git a/server/sonar-web/src/main/js/components/issue/Issue.css b/server/sonar-web/src/main/js/components/issue/Issue.css index 71c39dd6915..06861fd5599 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.css +++ b/server/sonar-web/src/main/js/components/issue/Issue.css @@ -31,6 +31,7 @@ .issue.selected { box-shadow: none; + outline: none; border: 2px solid var(--blue) !important; } |