]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19918 Use the issues/list endpoint for the source viewer when re-indexing
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Tue, 18 Jul 2023 14:46:46 +0000 (16:46 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Jul 2023 20:03:05 +0000 (20:03 +0000)
server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap
server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/loadIssues-test.ts
server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts
server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx [deleted file]
server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap [deleted file]

index cba93f8f92560419ebac2466870b54bc1318060e..052ca8605c4fb92fce53772079494731d53efe4e 100644 (file)
@@ -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<Props, State> {
+export class SourceViewerClass extends React.PureComponent<Props, State> {
   mounted = false;
 
   static defaultProps = {
@@ -150,6 +154,7 @@ export default class SourceViewer extends React.PureComponent<Props, State> {
     ) {
       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<Props, State> {
       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<Props, State> {
 
   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<Props, State> {
     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<Props, State> {
                 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<Props, State> {
 
     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<Props, State> {
       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<Props, State> {
     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<Props, State> {
     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<Props, State> {
     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<Props, State> {
     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<Props, State> {
       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<Props, State> {
           <DuplicationPopup
             blocks={filterDuplicationBlocksByLine(blocks, line)}
             branchLike={this.props.branchLike}
-            inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
             duplicatedFiles={duplicatedFiles}
+            duplicationHeader={translate('component_viewer.transition.duplication')}
+            inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
             openComponent={openComponent}
             sourceViewerFile={component}
-            duplicationHeader={translate('component_viewer.transition.duplication')}
           />
         )}
       </WorkspaceContext.Consumer>
@@ -495,6 +523,7 @@ export default class SourceViewer extends React.PureComponent<Props, State> {
 
   renderCode(sources: SourceLine[]) {
     const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
+
     return (
       <SourceViewerCode
         branchLike={this.props.branchLike}
@@ -513,21 +542,21 @@ export default class SourceViewer extends React.PureComponent<Props, State> {
         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<Props, State> {
       <SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
         <div className="source-viewer">
           {!hideHeader && this.renderHeader(component)}
+
           {sourceRemoved && (
             <Alert className="spacer-top" variant="warning">
               {translate('code_viewer.no_source_code_displayed_due_to_source_removed')}
             </Alert>
           )}
+
           {!sourceRemoved && sources !== undefined && this.renderCode(sources)}
         </div>
       </SourceViewerContext.Provider>
     );
   }
 }
+
+export default function SourceViewer(props: Props) {
+  return (
+    // we can't use withComponentContext as it would override the "component" prop
+    <ComponentContext.Consumer>
+      {({ component }) => <SourceViewerClass needIssueSync={component?.needIssueSync} {...props} />}
+    </ComponentContext.Consumer>
+  );
+}
index 39989709266d7dbd63512eeb06ff4effff04289c..ce8fd1957da698ed445abd8d87fe2df735fa8127 100644 (file)
@@ -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<SourceViewer['props']>) {
+function renderSourceViewer(override?: Partial<Props>) {
   const { rerender } = renderComponent(
     <SourceViewer
       aroundLine={1}
@@ -376,7 +405,8 @@ function renderSourceViewer(override?: Partial<SourceViewer['props']>) {
       {...override}
     />
   );
-  return function (reoverride?: Partial<SourceViewer['props']>) {
+
+  return function (reoverride?: Partial<Props>) {
     rerender(
       <SourceViewer
         aroundLine={1}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx
deleted file mode 100644 (file)
index a922644..0000000
+++ /dev/null
@@ -1,126 +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 { getComponentData, getComponentForSourceViewer, getSources } from '../../../api/components';
-import { mockMainBranch } from '../../../helpers/mocks/branch-like';
-import { mockSourceLine, mockSourceViewerFile } from '../../../helpers/mocks/sources';
-import { mockIssue } from '../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../helpers/testUtils';
-import defaultLoadIssues from '../helpers/loadIssues';
-import SourceViewer from '../SourceViewer';
-
-jest.mock('../helpers/loadIssues', () => 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<SourceViewer['props']> = {}) {
-  return shallow<SourceViewer>(
-    <SourceViewer branchLike={mockMainBranch()} component="my-component" {...overrides} />
-  );
-}
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 (file)
index a306b0f..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ContextProvider
-  value={
-    {
-      "branchLike": {
-        "analysisDate": "2018-01-01",
-        "excludedFromPurge": true,
-        "isMain": true,
-        "name": "master",
-      },
-      "file": {
-        "canMarkAsFavorite": true,
-        "fav": false,
-        "key": "project:foo/bar.ts",
-        "leakPeriodDate": "2018-06-20T17:12:19+0200",
-        "longName": "foo/bar.ts",
-        "measures": {
-          "coverage": "85.2",
-          "duplicationDensity": "1.0",
-          "issues": "12",
-          "lines": "56",
-        },
-        "name": "foo/bar.ts",
-        "path": "foo/bar.ts",
-        "project": "project",
-        "projectName": "MyProject",
-        "q": "FIL",
-        "uuid": "foo-bar",
-      },
-    }
-  }
->
-  <div
-    className="source-viewer"
-  >
-    <ContextConsumer>
-      <Component />
-    </ContextConsumer>
-    <SourceViewerCode
-      branchLike={
-        {
-          "analysisDate": "2018-01-01",
-          "excludedFromPurge": true,
-          "isMain": true,
-          "name": "master",
-        }
-      }
-      displayAllIssues={false}
-      displayLocationMarkers={true}
-      duplicationsByLine={{}}
-      hasSourcesAfter={false}
-      hasSourcesBefore={false}
-      highlightedSymbols={[]}
-      issueLocationsByLine={
-        {
-          "25": [
-            {
-              "from": 0,
-              "line": 25,
-              "to": 999999,
-            },
-          ],
-          "26": [
-            {
-              "from": 0,
-              "line": 26,
-              "to": 15,
-            },
-          ],
-        }
-      }
-      issues={
-        [
-          {
-            "actions": [],
-            "component": "main.js",
-            "componentEnabled": true,
-            "componentLongName": "main.js",
-            "componentQualifier": "FIL",
-            "componentUuid": "foo1234",
-            "creationDate": "2017-03-01T09:36:01+0100",
-            "flows": [],
-            "flowsWithType": [],
-            "key": "AVsae-CQS-9G3txfbFN2",
-            "line": 25,
-            "message": "Reduce the number of conditional operators (4) used in the expression",
-            "project": "myproject",
-            "projectKey": "foo",
-            "projectName": "Foo",
-            "rule": "javascript:S1067",
-            "ruleName": "foo",
-            "scope": "MAIN",
-            "secondaryLocations": [],
-            "severity": "MAJOR",
-            "status": "OPEN",
-            "textRange": {
-              "endLine": 26,
-              "endOffset": 15,
-              "startLine": 25,
-              "startOffset": 0,
-            },
-            "transitions": [],
-            "type": "BUG",
-          },
-        ]
-      }
-      issuesByLine={
-        {
-          "26": [
-            {
-              "actions": [],
-              "component": "main.js",
-              "componentEnabled": true,
-              "componentLongName": "main.js",
-              "componentQualifier": "FIL",
-              "componentUuid": "foo1234",
-              "creationDate": "2017-03-01T09:36:01+0100",
-              "flows": [],
-              "flowsWithType": [],
-              "key": "AVsae-CQS-9G3txfbFN2",
-              "line": 25,
-              "message": "Reduce the number of conditional operators (4) used in the expression",
-              "project": "myproject",
-              "projectKey": "foo",
-              "projectName": "Foo",
-              "rule": "javascript:S1067",
-              "ruleName": "foo",
-              "scope": "MAIN",
-              "secondaryLocations": [],
-              "severity": "MAJOR",
-              "status": "OPEN",
-              "textRange": {
-                "endLine": 26,
-                "endOffset": 15,
-                "startLine": 25,
-                "startOffset": 0,
-              },
-              "transitions": [],
-              "type": "BUG",
-            },
-          ],
-        }
-      }
-      loadDuplications={[Function]}
-      loadSourcesAfter={[Function]}
-      loadSourcesBefore={[Function]}
-      loadingSourcesAfter={false}
-      loadingSourcesBefore={false}
-      onIssueChange={[Function]}
-      onIssuePopupToggle={[Function]}
-      onIssueSelect={[Function]}
-      onIssueUnselect={[Function]}
-      onIssuesClose={[Function]}
-      onIssuesOpen={[Function]}
-      onSymbolClick={[Function]}
-      openIssuesByLine={{}}
-      renderDuplicationPopup={[Function]}
-      sources={[]}
-      symbolsByLine={{}}
-    />
-  </div>
-</ContextProvider>
-`;
index 179bdb11d7f5c709b23ee8438cf104c3a47ce6ce..20f82563531fea9887f908f27f43505ed7e63a12 100644 (file)
@@ -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": [
index 601deaf97dc57f9f5b9074619fd11bff8985e71f..190328d7ea88ec809669c60cddaaee4056cbf381 100644 (file)
  * 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();
   });
 });
index 70a873c7c05a9bc186d848a9a20ec84970213c61..7ed620c4009e7d42e0e6cb86c040df3bf46842b4 100644 (file)
@@ -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<Issue[]> {
+function loadListPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise<Issue[]> {
+  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<Issue[]> {
   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<Issue[]> {
-  const issues = await loadPage(query, page);
+async function loadPageAndNext(
+  query: RawQuery,
+  needIssueSync = false,
+  page = 1,
+  pageSize = PAGE_SIZE
+): Promise<Issue[]> {
+  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<Issue[]> {
-  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 (file)
index 5efd0bd..0000000
+++ /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<Function>('onClose')();
-  expect(onClose).toHaveBeenCalledWith('foo');
-});
-
-it('should call back after load', () => {
-  const onLoad = jest.fn();
-  const wrapper = shallowRender({ onLoad });
-  wrapper.find('[onLoaded]').prop<Function>('onLoaded')({
-    key: 'foo',
-    path: 'src/foo.js',
-    q: 'FIL',
-  });
-  expect(onLoad).toHaveBeenCalledWith({ key: 'foo', name: 'src/foo.js', qualifier: 'FIL' });
-});
-
-function shallowRender(props?: Partial<Props>) {
-  return shallow<WorkspaceComponentViewer>(
-    <WorkspaceComponentViewer
-      component={{ branchLike: undefined, key: 'foo' }}
-      height={300}
-      onClose={jest.fn()}
-      onCollapse={jest.fn()}
-      onLoad={jest.fn()}
-      onMaximize={jest.fn()}
-      onMinimize={jest.fn()}
-      onResize={jest.fn()}
-      {...props}
-    />
-  );
-}
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 (file)
index 65ba4da..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div
-  className="workspace-viewer"
->
-  <WorkspaceHeader
-    onClose={[Function]}
-    onCollapse={[MockFunction]}
-    onMaximize={[MockFunction]}
-    onMinimize={[MockFunction]}
-    onResize={[MockFunction]}
-  >
-    <WorkspaceComponentTitle
-      component={
-        {
-          "branchLike": undefined,
-          "key": "foo",
-        }
-      }
-    />
-  </WorkspaceHeader>
-  <div
-    className="workspace-viewer-container"
-    style={
-      {
-        "height": 300,
-      }
-    }
-  >
-    <SourceViewer
-      component="foo"
-      displayAllIssues={false}
-      displayIssueLocationsCount={true}
-      displayIssueLocationsLink={true}
-      displayLocationMarkers={true}
-      onLoaded={[Function]}
-    />
-  </div>
-</div>
-`;