From: David Cho-Lerat Date: Tue, 18 Jul 2023 14:46:46 +0000 (+0200) Subject: SONAR-19918 Use the issues/list endpoint for the source viewer when re-indexing X-Git-Tag: 10.2.0.77647~343 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=2ff980ad62afec5e4c145d3b1428a4a315ac0ed3;p=sonarqube.git SONAR-19918 Use the issues/list endpoint for the source viewer when re-indexing --- diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx index cba93f8f925..052ca8605c4 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx @@ -17,6 +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 { intersection } from 'lodash'; import * as React from 'react'; import { @@ -25,10 +26,12 @@ import { getDuplications, getSources, } from '../../api/components'; +import { ComponentContext } from '../../app/components/componentContext/ComponentContext'; import { getBranchLikeQuery, isSameBranchLike } from '../../helpers/branch-like'; import { translate } from '../../helpers/l10n'; import { HttpStatus } from '../../helpers/request'; import { BranchLike } from '../../types/branch-like'; +import { ComponentQualifier } from '../../types/component'; import { Dict, DuplicatedFile, @@ -63,25 +66,26 @@ import loadIssues from './helpers/loadIssues'; import './styles.css'; export interface Props { - hideHeader?: boolean; aroundLine?: number; branchLike: BranchLike | undefined; component: string; componentMeasures?: Measure[]; displayAllIssues?: boolean; displayLocationMarkers?: boolean; + hideHeader?: boolean; highlightedLine?: number; + highlightedLocationMessage?: { index: number; text: string | undefined }; // `undefined` elements mean they are located in a different file, - // but kept to maintaint the location indexes + // but kept to maintain the location indexes highlightedLocations?: (FlowLocation | undefined)[]; - highlightedLocationMessage?: { index: number; text: string | undefined }; - onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void; - onLocationSelect?: (index: number) => void; + metricKey?: string; + needIssueSync?: boolean; onIssueSelect?: (issueKey: string) => void; onIssueUnselect?: () => void; + onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void; + onLocationSelect?: (index: number) => void; selectedIssue?: string; showMeasures?: boolean; - metricKey?: string; } interface State { @@ -107,7 +111,7 @@ interface State { symbolsByLine: { [line: number]: string[] }; } -export default class SourceViewer extends React.PureComponent { +export class SourceViewerClass extends React.PureComponent { mounted = false; static defaultProps = { @@ -150,6 +154,7 @@ export default class SourceViewer extends React.PureComponent { ) { this.setState({ selectedIssue: this.props.selectedIssue }); } + if ( prevProps.component !== this.props.component || !isSameBranchLike(prevProps.branchLike, this.props.branchLike) @@ -161,8 +166,10 @@ export default class SourceViewer extends React.PureComponent { this.isLineOutsideOfRange(this.props.aroundLine) ) { const sources = await this.fetchSources().catch(() => []); + if (this.mounted) { const finalSources = sources.slice(0, LINES_TO_LOAD); + this.setState( { sources: sources.slice(0, LINES_TO_LOAD), @@ -207,6 +214,7 @@ export default class SourceViewer extends React.PureComponent { isLineOutsideOfRange(lineNumber: number) { const { sources } = this.state; + if (sources && sources.length > 0) { const firstLine = sources[0]; const lastList = sources[sources.length - 1]; @@ -220,10 +228,11 @@ export default class SourceViewer extends React.PureComponent { this.setState({ loading: true }); const loadIssuesCallback = (component: SourceViewerFile, sources: SourceLine[]) => { - loadIssues(this.props.component, this.props.branchLike).then( + loadIssues(this.props.component, this.props.branchLike, this.props.needIssueSync).then( (issues) => { if (this.mounted) { const finalSources = sources.slice(0, LINES_TO_LOAD); + this.setState( { component, @@ -233,13 +242,13 @@ export default class SourceViewer extends React.PureComponent { hasSourcesAfter: sources.length > LINES_TO_LOAD, highlightedSymbols: [], issueLocationsByLine: locationsByLine(issues), + issuePopup: undefined, issues, issuesByLine: issuesByLine(issues), loading: false, notAccessible: false, notExist: false, openIssuesByLine: {}, - issuePopup: undefined, sourceRemoved: false, sources: this.computeCoverageStatus(finalSources), symbolsByLine: symbolsByLine(sources.slice(0, LINES_TO_LOAD)), @@ -280,7 +289,10 @@ export default class SourceViewer extends React.PureComponent { const onResolve = (component: SourceViewerFile) => { const sourcesRequest = - component.q === 'FIL' || component.q === 'UTS' ? this.fetchSources() : Promise.resolve([]); + component.q === ComponentQualifier.File || component.q === ComponentQualifier.TestFile + ? this.fetchSources() + : Promise.resolve([]); + sourcesRequest.then( (sources) => loadIssuesCallback(component, sources), (response) => onFailLoadSources(response, component) @@ -312,10 +324,12 @@ export default class SourceViewer extends React.PureComponent { let to = this.props.aroundLine ? this.props.aroundLine + LINES_TO_LOAD / 2 + 1 : LINES_TO_LOAD + 1; + // make sure we try to download `LINES` lines if (from === 1 && to < LINES_TO_LOAD) { to = LINES_TO_LOAD; } + // request one additional line to define `hasSourcesAfter` to++; @@ -329,9 +343,13 @@ export default class SourceViewer extends React.PureComponent { if (!this.state.sources) { return; } + const firstSourceLine = this.state.sources[0]; + this.setState({ loadingSourcesBefore: true }); + const from = Math.max(1, firstSourceLine.line - LINES_TO_LOAD); + this.loadSources( this.props.component, from, @@ -359,17 +377,23 @@ export default class SourceViewer extends React.PureComponent { if (!this.state.sources) { return; } + const lastSourceLine = this.state.sources[this.state.sources.length - 1]; + this.setState({ loadingSourcesAfter: true }); + const fromLine = lastSourceLine.line + 1; const toLine = lastSourceLine.line + LINES_TO_LOAD + 1; + this.loadSources(this.props.component, fromLine, toLine, this.props.branchLike).then( (sources) => { if (this.mounted) { const hasSourcesAfter = LINES_TO_LOAD < sources.length; + if (hasSourcesAfter) { sources.pop(); } + this.setState((prevState) => { return { hasSourcesAfter, @@ -413,11 +437,13 @@ export default class SourceViewer extends React.PureComponent { this.setState((state: State) => { const samePopup = state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue; + if (open !== false && !samePopup) { return { issuePopup: { issue, name: popupName } }; } else if (open !== true && samePopup) { return { issuePopup: undefined }; } + return null; }); }; @@ -426,6 +452,7 @@ export default class SourceViewer extends React.PureComponent { this.setState((state) => { const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0; const highlightedSymbols = shouldDisable ? [] : symbols; + return { highlightedSymbols }; }); }; @@ -463,6 +490,7 @@ export default class SourceViewer extends React.PureComponent { const newIssues = issues.map((candidate) => candidate.key === issue.key ? issue : candidate ); + return { issues: newIssues, issuesByLine: issuesByLine(newIssues) }; }); }; @@ -482,11 +510,11 @@ export default class SourceViewer extends React.PureComponent { )} @@ -495,6 +523,7 @@ export default class SourceViewer extends React.PureComponent { renderCode(sources: SourceLine[]) { const hasSourcesBefore = sources.length > 0 && sources[0].line > 1; + return ( { issues={this.state.issues} issuesByLine={this.state.issuesByLine} loadDuplications={this.loadDuplications} - loadSourcesAfter={this.loadSourcesAfter} - loadSourcesBefore={this.loadSourcesBefore} loadingSourcesAfter={this.state.loadingSourcesAfter} loadingSourcesBefore={this.state.loadingSourcesBefore} + loadSourcesAfter={this.loadSourcesAfter} + loadSourcesBefore={this.loadSourcesBefore} + metricKey={this.props.metricKey} onIssueChange={this.handleIssueChange} onIssuePopupToggle={this.handleIssuePopupToggle} - onIssueSelect={this.handleIssueSelect} - onIssueUnselect={this.handleIssueUnselect} onIssuesClose={this.handleCloseIssues} + onIssueSelect={this.handleIssueSelect} onIssuesOpen={this.handleOpenIssues} + onIssueUnselect={this.handleIssueUnselect} onLocationSelect={this.props.onLocationSelect} onSymbolClick={this.handleSymbolClick} openIssuesByLine={this.state.openIssuesByLine} renderDuplicationPopup={this.renderDuplicationPopup} - metricKey={this.props.metricKey} selectedIssue={this.state.selectedIssue} sources={sources} symbolsByLine={this.state.symbolsByLine} @@ -583,14 +612,25 @@ export default class SourceViewer extends React.PureComponent {
{!hideHeader && this.renderHeader(component)} + {sourceRemoved && ( {translate('code_viewer.no_source_code_displayed_due_to_source_removed')} )} + {!sourceRemoved && sources !== undefined && this.renderCode(sources)}
); } } + +export default function SourceViewer(props: Props) { + return ( + // we can't use withComponentContext as it would override the "component" prop + + {({ component }) => } + + ); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx index 39989709266..ce8fd1957da 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx @@ -17,6 +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 { queryHelpers, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; @@ -27,7 +28,7 @@ import { HttpStatus } from '../../../helpers/request'; import { mockIssue } from '../../../helpers/testMocks'; import { renderComponent } from '../../../helpers/testReactTestingUtils'; import { byText } from '../../../helpers/testSelector'; -import SourceViewer from '../SourceViewer'; +import SourceViewer, { Props } from '../SourceViewer'; import loadIssues from '../helpers/loadIssues'; jest.mock('../../../api/components'); @@ -57,6 +58,7 @@ const ui = { const componentsHandler = new ComponentsServiceMock(); const issuesHandler = new IssuesServiceMock(); +const message = 'First Issue'; beforeEach(() => { issuesHandler.reset(); @@ -69,6 +71,7 @@ it('should show a permalink on line number', async () => { let row = await screen.findByRole('row', { name: /\/\*$/ }); expect(row).toBeInTheDocument(); const rowScreen = within(row); + await user.click( rowScreen.getByRole('button', { name: 'source_viewer.line_X.1', @@ -117,31 +120,34 @@ it('should show a permalink on line number', async () => { }); it('should show issue on empty file', async () => { - (loadIssues as jest.Mock).mockResolvedValueOnce([ + jest.mocked(loadIssues).mockResolvedValueOnce([ mockIssue(false, { key: 'first-issue', - message: 'First Issue', + message, line: undefined, textRange: undefined, }), ]); + renderSourceViewer({ component: componentsHandler.getEmptyFileKey(), }); + expect(await screen.findByRole('table')).toBeInTheDocument(); expect(await screen.findByRole('row', { name: 'First Issue' })).toBeInTheDocument(); }); it('should be able to interact with issue action', async () => { - (loadIssues as jest.Mock).mockResolvedValueOnce([ + jest.mocked(loadIssues).mockResolvedValueOnce([ mockIssue(false, { actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'], key: 'issue1', - message: 'First Issue', + message, line: 1, textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }, }), ]); + const user = userEvent.setup(); renderSourceViewer(); @@ -149,12 +155,14 @@ it('should be able to interact with issue action', async () => { await user.click( await screen.findByLabelText('issue.type.type_x_click_to_change.issue.type.BUG') ); + expect(ui.codeSmellTypeButton.get()).toBeInTheDocument(); // Open severity await user.click( await screen.findByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR') ); + expect(ui.minorSeverityButton.get()).toBeInTheDocument(); // Close @@ -165,8 +173,10 @@ it('should be able to interact with issue action', async () => { await user.click( await screen.findByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR') ); + expect(ui.minorSeverityButton.get()).toBeInTheDocument(); await user.click(ui.minorSeverityButton.get()); + expect( screen.getByLabelText('issue.severity.severity_x_click_to_change.severity.MINOR') ).toBeInTheDocument(); @@ -177,6 +187,7 @@ it('should load line when looking around unloaded line', async () => { aroundLine: 50, component: componentsHandler.getHugeFileKey(), }); + expect(await screen.findByRole('row', { name: /Line 50$/ })).toBeInTheDocument(); rerender({ aroundLine: 100, component: componentsHandler.getHugeFileKey() }); @@ -189,9 +200,11 @@ it('should show SCM information', async () => { let row = await screen.findByRole('row', { name: /\/\*$/ }); expect(row).toBeInTheDocument(); const firstRowScreen = within(row); + expect( firstRowScreen.getByRole('cell', { name: 'stas.vilchik@sonarsource.com' }) ).toBeInTheDocument(); + await user.click( firstRowScreen.getByRole('button', { name: 'source_viewer.author_X.stas.vilchik@sonarsource.com, source_viewer.click_for_scm_info.1', @@ -206,6 +219,7 @@ it('should show SCM information', async () => { row = screen.getByRole('row', { name: /\* SonarQube$/ }); expect(row).toBeInTheDocument(); const secondRowScreen = within(row); + expect( secondRowScreen.queryByRole('cell', { name: 'stas.vilchik@sonarsource.com' }) ).not.toBeInTheDocument(); @@ -214,6 +228,7 @@ it('should show SCM information', async () => { row = await screen.findByRole('row', { name: /\* mailto:info AT sonarsource DOT com$/ }); expect(row).toBeInTheDocument(); const fourthRowScreen = within(row); + await act(async () => { await user.click( fourthRowScreen.getByRole('button', { @@ -227,6 +242,7 @@ it('should show SCM information', async () => { expect(row).toBeInTheDocument(); const fithRowScreen = within(row); expect(fithRowScreen.getByText('…')).toBeInTheDocument(); + await act(async () => { await user.click( fithRowScreen.getByRole('button', { @@ -239,15 +255,16 @@ it('should show SCM information', async () => { row = await screen.findByRole('row', { name: /\* This program is free software; you can redistribute it and\/or$/, }); + expect(row).toBeInTheDocument(); expect(within(row).queryByRole('button')).not.toBeInTheDocument(); }); it('should show issue indicator', async () => { - (loadIssues as jest.Mock).mockResolvedValueOnce([ + jest.mocked(loadIssues).mockResolvedValueOnce([ mockIssue(false, { key: 'first-issue', - message: 'First Issue', + message, line: 1, textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }, }), @@ -258,15 +275,19 @@ it('should show issue indicator', async () => { textRange: { startLine: 1, endLine: 1, startOffset: 1, endOffset: 2 }, }), ]); + const user = userEvent.setup(); const onIssueSelect = jest.fn(); + renderSourceViewer({ onIssueSelect, displayAllIssues: false, }); + const row = await screen.findByRole('row', { name: /.*\/ \*$/ }); const issueRow = within(row); expect(issueRow.getByText('2')).toBeInTheDocument(); + await user.click( issueRow.getByRole('button', { name: 'source_viewer.issues_on_line.X_issues_of_type_Y.source_viewer.issues_on_line.show.2.issue.type.BUG.plural', @@ -276,9 +297,11 @@ it('should show issue indicator', async () => { it('should show coverage information', async () => { renderSourceViewer(); + const coverdLine = within( await screen.findByRole('row', { name: /\* mailto:info AT sonarsource DOT com$/ }) ); + expect( coverdLine.getByLabelText('source_viewer.tooltip.covered.conditions.1') ).toBeInTheDocument(); @@ -286,6 +309,7 @@ it('should show coverage information', async () => { const partialyCoveredWithConditionLine = within( await screen.findByRole('row', { name: / \* 5$/ }) ); + expect( partialyCoveredWithConditionLine.getByLabelText( 'source_viewer.tooltip.partially-covered.conditions.1.2' @@ -293,6 +317,7 @@ it('should show coverage information', async () => { ).toBeInTheDocument(); const partialyCoveredLine = within(await screen.findByRole('row', { name: /\/\*$/ })); + expect( partialyCoveredLine.getByLabelText('source_viewer.tooltip.partially-covered') ).toBeInTheDocument(); @@ -303,11 +328,13 @@ it('should show coverage information', async () => { const uncoveredWithConditionLine = within( await screen.findByRole('row', { name: / \* SonarQube$/ }) ); + expect( uncoveredWithConditionLine.getByLabelText('source_viewer.tooltip.uncovered.conditions.1') ).toBeInTheDocument(); const coveredWithNoCondition = within(await screen.findByRole('row', { name: /\* Copyright$/ })); + expect( coveredWithNoCondition.getByLabelText('source_viewer.tooltip.covered') ).toBeInTheDocument(); @@ -317,6 +344,7 @@ it('should show duplication block', async () => { const user = userEvent.setup(); renderSourceViewer(); const duplicateLine = within(await screen.findByRole('row', { name: /\* 7$/ })); + expect( duplicateLine.getByLabelText('source_viewer.tooltip.duplicated_block') ).toBeInTheDocument(); @@ -351,6 +379,7 @@ it('should highlight symbol', async () => { it('should show correct message when component is not asscessible', async () => { componentsHandler.setFailLoadingComponentStatus(HttpStatus.Forbidden); renderSourceViewer(); + expect( await screen.findByText('code_viewer.no_source_code_displayed_due_to_security') ).toBeInTheDocument(); @@ -362,7 +391,7 @@ it('should show correct message when component does not exist', async () => { expect(await screen.findByText('component_viewer.no_component')).toBeInTheDocument(); }); -function renderSourceViewer(override?: Partial) { +function renderSourceViewer(override?: Partial) { const { rerender } = renderComponent( ) { {...override} /> ); - return function (reoverride?: Partial) { + + return function (reoverride?: Partial) { rerender( jest.fn().mockRejectedValue({})); - -jest.mock('../../../api/components', () => ({ - getComponentForSourceViewer: jest.fn().mockRejectedValue(''), - getComponentData: jest.fn().mockRejectedValue(''), - getSources: jest.fn().mockRejectedValue(''), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -it('should render nothing from the start', () => { - expect(shallowRender().type()).toBeNull(); -}); - -it('should render correctly', async () => { - (defaultLoadIssues as jest.Mock).mockResolvedValueOnce([mockIssue()]); - (getComponentForSourceViewer as jest.Mock).mockResolvedValueOnce(mockSourceViewerFile()); - (getComponentData as jest.Mock).mockResolvedValueOnce({ - component: { leakPeriodDate: '2018-06-20T17:12:19+0200' }, - }); - (getSources as jest.Mock).mockResolvedValueOnce([]); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - - expect(wrapper).toMatchSnapshot(); -}); - -it('should load sources before', async () => { - (defaultLoadIssues as jest.Mock).mockResolvedValueOnce([ - mockIssue(false, { key: 'issue1' }), - mockIssue(false, { key: 'issue2' }), - ]); - (getComponentForSourceViewer as jest.Mock).mockResolvedValueOnce(mockSourceViewerFile()); - (getComponentData as jest.Mock).mockResolvedValueOnce({ - component: { leakPeriodDate: '2018-06-20T17:12:19+0200' }, - }); - (getSources as jest.Mock) - .mockResolvedValueOnce([mockSourceLine()]) - .mockResolvedValueOnce([mockSourceLine()]); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - - wrapper.instance().loadSourcesBefore(); - expect(wrapper.state().loadingSourcesBefore).toBe(true); - - expect(defaultLoadIssues).toHaveBeenCalledTimes(1); - expect(getSources).toHaveBeenCalledTimes(2); - - await waitAndUpdate(wrapper); - expect(wrapper.state().loadingSourcesBefore).toBe(false); - expect(wrapper.state().issues).toHaveLength(2); -}); - -it('should load sources after', async () => { - (defaultLoadIssues as jest.Mock).mockResolvedValueOnce([ - mockIssue(false, { key: 'issue1' }), - mockIssue(false, { key: 'issue2' }), - ]); - (getComponentForSourceViewer as jest.Mock).mockResolvedValueOnce(mockSourceViewerFile()); - (getComponentData as jest.Mock).mockResolvedValueOnce({ - component: { leakPeriodDate: '2018-06-20T17:12:19+0200' }, - }); - (getSources as jest.Mock) - .mockResolvedValueOnce([mockSourceLine()]) - .mockResolvedValueOnce([mockSourceLine()]); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - - wrapper.instance().loadSourcesAfter(); - expect(wrapper.state().loadingSourcesAfter).toBe(true); - - expect(defaultLoadIssues).toHaveBeenCalledTimes(1); - expect(getSources).toHaveBeenCalledTimes(2); - - await waitAndUpdate(wrapper); - - expect(wrapper.state().loadingSourcesAfter).toBe(false); - expect(wrapper.state().issues).toHaveLength(2); -}); - -it('should handle no sources when checking ranges', () => { - const wrapper = shallowRender(); - - wrapper.setState({ sources: undefined }); - expect(wrapper.instance().isLineOutsideOfRange(12)).toBe(true); -}); - -function shallowRender(overrides: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap deleted file mode 100644 index a306b0f3a1e..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap +++ /dev/null @@ -1,165 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - -
- - - - -
-
-`; 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 index 179bdb11d7f..20f82563531 100644 --- 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 @@ -1,6 +1,59 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`loadIssues should load issues 1`] = ` +exports[`loadIssues should load issues with listIssues if re-indexing 1`] = ` +[ + { + "actions": [ + "set_tags", + "comment", + "assign", + ], + "assignee": "luke", + "author": "luke@sonarsource.com", + "comments": [], + "component": "foo.java", + "componentEnabled": true, + "componentKey": "foo.java", + "componentLongName": "Foo.java", + "componentName": "foo.java", + "componentPath": "/foo.java", + "componentQualifier": "FIL", + "creationDate": "2016-08-15T15:25:38+0200", + "flows": [], + "flowsWithType": [], + "hash": "78417dcee7ba927b7e7c9161e29e02b8", + "key": "AWaqVGl3tut9VbnJvk6M", + "line": 62, + "message": "Make sure this file handling is safe here.", + "project": "org.sonarsource.java:java", + "projectEnabled": true, + "projectKey": "org.sonarsource.java:java", + "projectLongName": "SonarJava", + "projectName": "SonarJava", + "projectQualifier": "TRK", + "rule": "squid:S4797", + "secondaryLocations": [], + "status": "OPEN", + "tags": [ + "cert", + "cwe", + "owasp-a1", + "owasp-a3", + ], + "textRange": { + "endLine": 62, + "endOffset": 96, + "startLine": 62, + "startOffset": 93, + }, + "transitions": [], + "type": "SECURITY_HOTSPOT", + "updateDate": "2018-10-25T10:23:08+0200", + }, +] +`; + +exports[`loadIssues should load issues with searchIssues if not re-indexing 1`] = ` [ { "actions": [ 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 index 601deaf97dc..190328d7ea8 100644 --- 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 @@ -17,72 +17,87 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; +import { ComponentQualifier } from '../../../../types/component'; import loadIssues from '../loadIssues'; +const mockListResolvedValue = { + components: [ + { + enabled: true, + key: 'org.sonarsource.java:java', + longName: 'SonarJava', + name: 'SonarJava', + qualifier: ComponentQualifier.Project, + }, + { + enabled: true, + key: 'foo.java', + longName: 'Foo.java', + name: 'foo.java', + path: '/foo.java', + qualifier: ComponentQualifier.File, + }, + ], + issues: [ + { + actions: ['set_tags', 'comment', 'assign'], + assignee: 'luke', + author: 'luke@sonarsource.com', + comments: [], + component: 'foo.java', + creationDate: '2016-08-15T15:25:38+0200', + flows: [], + hash: '78417dcee7ba927b7e7c9161e29e02b8', + key: 'AWaqVGl3tut9VbnJvk6M', + line: 62, + message: 'Make sure this file handling is safe here.', + project: 'org.sonarsource.java:java', + rule: 'squid:S4797', + status: 'OPEN', + tags: ['cert', 'cwe', 'owasp-a1', 'owasp-a3'], + textRange: { startLine: 62, endLine: 62, startOffset: 93, endOffset: 96 }, + transitions: [], + type: 'SECURITY_HOTSPOT', + updateDate: '2018-10-25T10:23:08+0200', + }, + ], + paging: { pageIndex: 1, pageSize: 500, total: 1 }, +}; + +const mockSearchResolvedValue = { + ...mockListResolvedValue, + debtTotal: 15, + effortTotal: 15, + facets: [], + languages: [{ key: 'java', name: 'Java' }], + rules: [ + { + key: 'squid:S4797', + lang: 'java', + langName: 'Java', + name: 'Handling files is security-sensitive', + status: 'READY', + }, + ], + users: [{ active: true, avatar: 'lukavatar', login: 'luke', name: 'Luke' }], +}; + 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', - }, - ], - components: [ - { - key: 'org.sonarsource.java:java', - enabled: true, - qualifier: 'TRK', - name: 'SonarJava', - longName: 'SonarJava', - }, - { - 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: [], - }), + listIssues: jest.fn().mockImplementation(() => Promise.resolve(mockListResolvedValue)), + searchIssues: jest.fn().mockImplementation(() => Promise.resolve(mockSearchResolvedValue)), })); describe('loadIssues', () => { - it('should load issues', async () => { + it('should load issues with searchIssues if not re-indexing', async () => { const result = await loadIssues('foo.java', mockMainBranch()); + + expect(result).toMatchSnapshot(); + }); + + it('should load issues with listIssues if re-indexing', async () => { + const result = await loadIssues('foo.java', mockMainBranch(), true); expect(result).toMatchSnapshot(); }); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts index 70a873c7c05..7ed620c4009 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { searchIssues } from '../../../api/issues'; + +import { listIssues, searchIssues } from '../../../api/issues'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { parseIssueFromResponse } from '../../../helpers/issues'; import { BranchLike } from '../../../types/branch-like'; @@ -28,17 +29,33 @@ const PAGE_SIZE = 500; // Maximum issues return 20*500 for the API. const PAGE_MAX = 20; -function buildQuery(component: string, branchLike: BranchLike | undefined) { +function buildListQuery(component: string, branchLike: BranchLike | undefined) { return { - additionalFields: '_all', + component, resolved: 'false', + ...getBranchLikeQuery(branchLike), + }; +} + +function buildSearchQuery(component: string, branchLike: BranchLike | undefined) { + return { + additionalFields: '_all', componentKeys: component, + resolved: 'false', s: 'FILE_LINE', ...getBranchLikeQuery(branchLike), }; } -function loadPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise { +function loadListPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise { + return listIssues({ + ...query, + p: page, + ps: pageSize, + }).then((r) => r.issues.map((issue) => parseIssueFromResponse(issue, r.components))); +} + +function loadSearchPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise { return searchIssues({ ...query, p: page, @@ -48,8 +65,15 @@ function loadPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise< ); } -async function loadPageAndNext(query: RawQuery, page = 1, pageSize = PAGE_SIZE): Promise { - const issues = await loadPage(query, page); +async function loadPageAndNext( + query: RawQuery, + needIssueSync = false, + page = 1, + pageSize = PAGE_SIZE +): Promise { + const issues = needIssueSync + ? await loadListPage(query, page) + : await loadSearchPage(query, page); if (issues.length === 0) { return []; @@ -59,14 +83,19 @@ async function loadPageAndNext(query: RawQuery, page = 1, pageSize = PAGE_SIZE): return issues; } - const nextIssues = await loadPageAndNext(query, page + 1, pageSize); + const nextIssues = await loadPageAndNext(query, needIssueSync, page + 1, pageSize); + return [...issues, ...nextIssues]; } export default function loadIssues( component: string, - branchLike: BranchLike | undefined + branchLike: BranchLike | undefined, + needIssueSync = false ): Promise { - const query = buildQuery(component, branchLike); - return loadPageAndNext(query); + const query = needIssueSync + ? buildListQuery(component, branchLike) + : buildSearchQuery(component, branchLike); + + return loadPageAndNext(query, needIssueSync); } diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx deleted file mode 100644 index 5efd0bdb0e6..00000000000 --- a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 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 WorkspaceComponentViewer, { Props } from '../WorkspaceComponentViewer'; - -jest.mock('../../../api/components', () => ({ - getParents: jest.fn().mockResolvedValue([{ key: 'bar' }]), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should close', () => { - const onClose = jest.fn(); - const wrapper = shallowRender({ onClose }); - wrapper.find('WorkspaceHeader').prop('onClose')(); - expect(onClose).toHaveBeenCalledWith('foo'); -}); - -it('should call back after load', () => { - const onLoad = jest.fn(); - const wrapper = shallowRender({ onLoad }); - wrapper.find('[onLoaded]').prop('onLoaded')({ - key: 'foo', - path: 'src/foo.js', - q: 'FIL', - }); - expect(onLoad).toHaveBeenCalledWith({ key: 'foo', name: 'src/foo.js', qualifier: 'FIL' }); -}); - -function shallowRender(props?: Partial) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap deleted file mode 100644 index 65ba4da4254..00000000000 --- a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render 1`] = ` -
- - - -
- -
-
-`;