aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2022-06-21 09:24:30 +0200
committersonartech <sonartech@sonarsource.com>2022-07-20 20:03:25 +0000
commit0a7d6f7245e753feb25bceae733fd659591b8f81 (patch)
tree1f40d8439a8d7543dd0828a884868aada5a1810f
parent466c012d87ca3f37ead691160afb87b212d01645 (diff)
downloadsonarqube-0a7d6f7245e753feb25bceae733fd659591b8f81.tar.gz
sonarqube-0a7d6f7245e753feb25bceae733fd659591b8f81.zip
SONAR-16475 Remove lazyLoading of frontend component
-rw-r--r--server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts168
-rw-r--r--server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts50
-rw-r--r--server/sonar-web/src/main/js/app/components/App.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/StartupModal.tsx7
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx7
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/TaskActions-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts71
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap530
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/__tests__/MeasuresApp-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx283
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx296
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx (renamed from server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx)8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/issues/routes.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/App.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx7
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx653
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx673
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx84
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx (renamed from server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerBase-test.tsx)8
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap (renamed from server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerBase-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/components/__tests__/__snapshots__/lazyLoadComponent-test.tsx.snap36
-rw-r--r--server/sonar-web/src/main/js/components/__tests__/lazyLoadComponent-test.tsx65
-rw-r--r--server/sonar-web/src/main/js/components/controls/DateInput.tsx5
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap65
-rw-r--r--server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/lazyLoadComponent.tsx73
-rw-r--r--server/sonar-web/src/main/js/components/ui/CoverageRating.tsx7
-rw-r--r--server/sonar-web/src/main/js/components/workspace/Workspace.tsx14
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts15
-rw-r--r--server/sonar-web/src/main/js/helpers/browser.ts2
-rw-r--r--server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx35
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.ts4
62 files changed, 1479 insertions, 1954 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts b/server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts
new file mode 100644
index 00000000000..7a4271cc6a0
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts
@@ -0,0 +1,168 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { cloneDeep, flatMap, map } from 'lodash';
+import { mockComponent, mockComponentMeasure } from '../../helpers/mocks/component';
+import { ComponentQualifier } from '../../types/component';
+import { RawIssuesResponse } from '../../types/issues';
+import { ComponentMeasure, Metric, Paging } from '../../types/types';
+import { getChildren, getComponentTree } from '../components';
+import { searchIssues } from '../issues';
+
+interface ComponentTree {
+ component: ComponentMeasure;
+ child: ComponentTree[];
+}
+
+function isLeaf(node: ComponentTree) {
+ return node.child.length === 0;
+}
+
+function listChildComponent(node: ComponentTree): ComponentMeasure[] {
+ return map(node.child, n => n.component);
+}
+
+function listAllComponent(node: ComponentTree): ComponentMeasure[] {
+ if (isLeaf(node)) {
+ return [node.component];
+ }
+
+ return [node.component, ...flatMap(node.child, listAllComponent)];
+}
+
+function listLeavesComponent(node: ComponentTree): ComponentMeasure[] {
+ if (isLeaf(node)) {
+ return [node.component];
+ }
+ return flatMap(node.child, listLeavesComponent);
+}
+
+export default class CodeServiceMock {
+ componentTree: ComponentTree;
+
+ constructor() {
+ this.componentTree = {
+ component: mockComponentMeasure(),
+ child: [
+ {
+ component: mockComponentMeasure(false, {
+ key: 'foo:folerA',
+ name: 'folderA',
+ path: 'folderA',
+ qualifier: ComponentQualifier.Directory
+ }),
+ child: [
+ {
+ component: mockComponentMeasure(true, {
+ key: 'foo:folderA/out.tsx',
+ name: 'out.tsx',
+ path: 'folderA/out.tsx'
+ }),
+ child: []
+ }
+ ]
+ },
+ {
+ component: mockComponentMeasure(true, {
+ key: 'foo:index.tsx',
+ name: 'index.tsx',
+ path: 'index.tsx'
+ }),
+ child: []
+ }
+ ]
+ };
+ (getComponentTree as jest.Mock).mockImplementation(this.handleGetComponentTree);
+ (getChildren as jest.Mock).mockImplementation(this.handleGetChildren);
+ (searchIssues as jest.Mock).mockImplementation(this.handleSearchIssue);
+ }
+
+ findBaseComponent(key: string, from = this.componentTree): ComponentTree | undefined {
+ if (from.component.key === key) {
+ return from;
+ }
+ return from.child.find(node => this.findBaseComponent(key, node));
+ }
+
+ handleGetChildren = (
+ component: string
+ ): Promise<{
+ baseComponent: ComponentMeasure;
+ components: ComponentMeasure[];
+ metrics: Metric[];
+ paging: Paging;
+ }> => {
+ return this.handleGetComponentTree('children', component);
+ };
+
+ handleGetComponentTree = (
+ strategy: string,
+ component: string
+ ): Promise<{
+ baseComponent: ComponentMeasure;
+ components: ComponentMeasure[];
+ metrics: Metric[];
+ paging: Paging;
+ }> => {
+ const base = this.findBaseComponent(component);
+ let components: ComponentMeasure[] = [];
+ if (base === undefined) {
+ return Promise.reject({
+ errors: [{ msg: `No component has been found for id ${component}` }]
+ });
+ }
+ if (strategy === 'all' || strategy === '') {
+ components = listAllComponent(base);
+ } else if (strategy === 'children') {
+ components = listChildComponent(base);
+ } else if (strategy === 'leaves') {
+ components = listLeavesComponent(base);
+ }
+
+ return this.reply({
+ baseComponent: base.component,
+ components,
+ metrics: [],
+ paging: {
+ pageIndex: 1,
+ pageSize: 100,
+ total: components.length
+ }
+ });
+ };
+
+ handleSearchIssue = (): Promise<RawIssuesResponse> => {
+ return this.reply({
+ components: [],
+ effortTotal: 1,
+ facets: [],
+ issues: [],
+ languages: [],
+ paging: { total: 0, pageIndex: 1, pageSize: 100 }
+ });
+ };
+
+ getRootComponent() {
+ return mockComponent(this.componentTree.component);
+ }
+
+ reply<T>(response: T): Promise<T> {
+ return Promise.resolve(cloneDeep(response));
+ }
+}
diff --git a/server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts
index 7179d0f26e0..7205b908eb1 100644
--- a/server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts
@@ -27,13 +27,15 @@ import {
getSources
} from '../../api/components';
import { mockSourceLine } from '../../helpers/mocks/sources';
+import { HttpStatus } from '../../helpers/request';
import { BranchParameters } from '../../types/branch-like';
import { Dict } from '../../types/types';
+import { setIssueSeverity } from '../issues';
-function mockSourceFileView(name: string) {
+function mockSourceFileView(name: string, project = 'project') {
return {
component: {
- key: `project:${name}`,
+ key: `${project}:${name}`,
name,
qualifier: 'FIL',
path: name,
@@ -44,13 +46,13 @@ function mockSourceFileView(name: string) {
needIssueSync: false
},
sourceFileView: {
- key: `project:${name}`,
- uuid: 'AWMgNpveti8CNlpVyHAm',
+ key: `${project}:${name}`,
+ uuid: `AWMgNpveti8CNlpVyHAm${project}${name}`,
path: name,
name,
longName: name,
q: 'FIL',
- project: 'project',
+ project,
projectName: 'Test project',
fav: false,
canMarkAsFavorite: true,
@@ -89,6 +91,26 @@ const FILES: Dict<any> = {
),
ancestors: ANCESTORS
},
+ 'foo:index.tsx': {
+ ...mockSourceFileView('index.tsx', 'foo'),
+ sources: times(200, n =>
+ mockSourceLine({
+ line: n,
+ code: 'function Test() {}'
+ })
+ ),
+ ancestors: ANCESTORS
+ },
+ 'project:testSymb.tsx': {
+ ...mockSourceFileView('testSymb.tsx'),
+ sources: times(20, n =>
+ mockSourceLine({
+ line: n,
+ code: ' <span class="sym-35 sym">symbole</span>'
+ })
+ ),
+ ancestors: ANCESTORS
+ },
'project:test.js': {
...mockSourceFileView('test.js'),
sources: [
@@ -202,6 +224,8 @@ const FILES: Dict<any> = {
};
export class SourceViewerServiceMock {
+ faileLoadingComponentStatus: HttpStatus | undefined = undefined;
+
constructor() {
(getComponentData as jest.Mock).mockImplementation(this.handleGetComponentData);
(getComponentForSourceViewer as jest.Mock).mockImplementation(
@@ -209,6 +233,15 @@ export class SourceViewerServiceMock {
);
(getDuplications as jest.Mock).mockImplementation(this.handleGetDuplications);
(getSources as jest.Mock).mockImplementation(this.handleGetSources);
+ (setIssueSeverity as jest.Mock).mockImplementation(this.handleSetIssueSeverity);
+ }
+
+ reset() {
+ this.faileLoadingComponentStatus = undefined;
+ }
+
+ setFailLoadingComponentStatus(status: HttpStatus.Forbidden | HttpStatus.NotFound) {
+ this.faileLoadingComponentStatus = status;
}
getHugeFile(): string {
@@ -239,9 +272,16 @@ export class SourceViewerServiceMock {
};
handleGetComponentData = (data: { component: string } & BranchParameters) => {
+ if (this.faileLoadingComponentStatus !== undefined) {
+ return Promise.reject({ status: this.faileLoadingComponentStatus });
+ }
return this.reply(pick(FILES[data.component], ['component', 'ancestor']));
};
+ handleSetIssueSeverity = () => {
+ return this.reply({});
+ };
+
reply<T>(response: T): Promise<T> {
return Promise.resolve(cloneDeep(response));
}
diff --git a/server/sonar-web/src/main/js/app/components/App.tsx b/server/sonar-web/src/main/js/app/components/App.tsx
index ca2b0ab0daf..297252eba06 100644
--- a/server/sonar-web/src/main/js/app/components/App.tsx
+++ b/server/sonar-web/src/main/js/app/components/App.tsx
@@ -19,13 +19,11 @@
*/
import * as React from 'react';
import { Outlet } from 'react-router-dom';
-import { lazyLoadComponent } from '../../components/lazyLoadComponent';
import { AppState } from '../../types/appstate';
import { GlobalSettingKeys } from '../../types/settings';
import withAppStateContext from './app-state/withAppStateContext';
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
-
-const PageTracker = lazyLoadComponent(() => import('./PageTracker'));
+import PageTracker from './PageTracker';
interface Props {
appState: AppState;
diff --git a/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx b/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx
index 6efc8feff87..eb4a085f337 100644
--- a/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx
@@ -19,10 +19,8 @@
*/
import * as React from 'react';
import { Outlet } from 'react-router-dom';
-import { lazyLoadComponent } from '../../components/lazyLoadComponent';
import GlobalFooter from './GlobalFooter';
-
-const PageTracker = lazyLoadComponent(() => import('./PageTracker'));
+import PageTracker from './PageTracker';
export default function SimpleSessionsContainer() {
return (
diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx
index 5e6bfb55512..f52d1cbce65 100644
--- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx
+++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx
@@ -20,8 +20,8 @@
import { differenceInDays } from 'date-fns';
import * as React from 'react';
import { showLicense } from '../../api/editions';
+import LicensePromptModal from '../../apps/marketplace/components/LicensePromptModal';
import { Location, Router, withRouter } from '../../components/hoc/withRouter';
-import { lazyLoadComponent } from '../../components/lazyLoadComponent';
import { parseDate, toShortNotSoISOString } from '../../helpers/dates';
import { hasMessage } from '../../helpers/l10n';
import { get, save } from '../../helpers/storage';
@@ -31,11 +31,6 @@ import { CurrentUser, isLoggedIn } from '../../types/users';
import withAppStateContext from './app-state/withAppStateContext';
import withCurrentUserContext from './current-user/withCurrentUserContext';
-const LicensePromptModal = lazyLoadComponent(
- () => import('../../apps/marketplace/components/LicensePromptModal'),
- 'LicensePromptModal'
-);
-
interface StateProps {
currentUser: CurrentUser;
}
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.tsx b/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.tsx
index dca57e11428..edf25e7336e 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.tsx
+++ b/server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.tsx
@@ -20,7 +20,7 @@
import { screen } from '@testing-library/react';
import React from 'react';
import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../helpers/globalMessages';
-import { renderComponentApp } from '../../../helpers/testReactTestingUtils';
+import { renderApp } from '../../../helpers/testReactTestingUtils';
function NullComponent() {
return null;
@@ -30,7 +30,7 @@ it('should display messages', () => {
jest.useFakeTimers();
// we render anything, the GlobalMessageContainer is rendered independently from routing
- renderComponentApp('sonarqube', <NullComponent />);
+ renderApp('sonarqube', <NullComponent />);
addGlobalErrorMessage('This is an error');
addGlobalSuccessMessage('This was a triumph!');
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap
index d322409ceb1..1937a131c98 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -2,7 +2,7 @@
exports[`should render correctly: default 1`] = `
<Fragment>
- <LazyComponentWrapper />
+ <withRouter(withAppStateContext(PageTracker)) />
<Outlet />
<KeyboardShortcutsModal />
</Fragment>
@@ -10,12 +10,12 @@ exports[`should render correctly: default 1`] = `
exports[`should render correctly: with gravatar 1`] = `
<Fragment>
- <LazyComponentWrapper>
+ <withRouter(withAppStateContext(PageTracker))>
<link
href="http://example.com"
rel="preconnect"
/>
- </LazyComponentWrapper>
+ </withRouter(withAppStateContext(PageTracker))>
<Outlet />
<KeyboardShortcutsModal />
</Fragment>
diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx
index 62f4375af20..fe5dce024dd 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx
@@ -20,7 +20,7 @@
import { screen } from '@testing-library/react';
import * as React from 'react';
import { mockAppState } from '../../../../helpers/testMocks';
-import { renderComponentApp } from '../../../../helpers/testReactTestingUtils';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { Extension } from '../../../../types/types';
import GlobalPageExtension, { GlobalPageExtensionProps } from '../GlobalPageExtension';
@@ -58,12 +58,8 @@ function renderGlobalPageExtension(
globalPages: Extension[] = [],
params?: GlobalPageExtensionProps['params']
) {
- renderComponentApp(
- `extension/:pluginKey/:extensionKey`,
- <GlobalPageExtension params={params} />,
- {
- appState: mockAppState({ globalPages }),
- navigateTo
- }
- );
+ renderApp(`extension/:pluginKey/:extensionKey`, <GlobalPageExtension params={params} />, {
+ appState: mockAppState({ globalPages }),
+ navigateTo
+ });
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx
index 4d60b9080ae..abe6c291e8c 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx
@@ -19,16 +19,11 @@
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { lazyLoadComponent } from '../../../../components/lazyLoadComponent';
+import AnalysisWarningsModal from '../../../../components/common/AnalysisWarningsModal';
import { Alert } from '../../../../components/ui/Alert';
import { translate } from '../../../../helpers/l10n';
import { TaskWarning } from '../../../../types/tasks';
-const AnalysisWarningsModal = lazyLoadComponent(
- () => import('../../../../components/common/AnalysisWarningsModal'),
- 'AnalysisWarningsModal'
-);
-
interface Props {
componentKey: string;
isBranch: boolean;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap
index 4a4509f61f3..77a6cb35ba6 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap
@@ -31,7 +31,7 @@ exports[`should render 1`] = `
}
/>
</Alert>
- <AnalysisWarningsModal
+ <withCurrentUserContext(AnalysisWarningsModal)
componentKey="foo"
onClose={[Function]}
onWarningDismiss={[MockFunction]}
diff --git a/server/sonar-web/src/main/js/app/components/search/Search.tsx b/server/sonar-web/src/main/js/app/components/search/Search.tsx
index cc7d82729ba..eb716f341e7 100644
--- a/server/sonar-web/src/main/js/app/components/search/Search.tsx
+++ b/server/sonar-web/src/main/js/app/components/search/Search.tsx
@@ -26,7 +26,6 @@ import OutsideClickHandler from '../../../components/controls/OutsideClickHandle
import SearchBox from '../../../components/controls/SearchBox';
import { Router, withRouter } from '../../../components/hoc/withRouter';
import ClockIcon from '../../../components/icons/ClockIcon';
-import { lazyLoadComponent } from '../../../components/lazyLoadComponent';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../../helpers/keycodes';
@@ -37,15 +36,13 @@ import { ComponentQualifier } from '../../../types/component';
import { Dict } from '../../../types/types';
import RecentHistory from '../RecentHistory';
import './Search.css';
+import SearchResult from './SearchResult';
+import SearchResults from './SearchResults';
import { ComponentResult, More, Results, sortQualifiers } from './utils';
-const SearchResults = lazyLoadComponent(() => import('./SearchResults'));
-const SearchResult = lazyLoadComponent(() => import('./SearchResult'));
-
interface Props {
router: Router;
}
-
interface State {
loading: boolean;
loadingMore?: string;
diff --git a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
index 3fc8c1bbe54..236dafea32f 100644
--- a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
+++ b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
@@ -26,7 +26,7 @@ import NotificationsMock from '../../../api/mocks/NotificationsMock';
import UserTokensMock from '../../../api/mocks/UserTokensMock';
import { mockUserToken } from '../../../helpers/mocks/token';
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
-import { renderApp } from '../../../helpers/testReactTestingUtils';
+import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { Permissions } from '../../../types/permissions';
import { TokenType } from '../../../types/token';
import { CurrentUser } from '../../../types/users';
@@ -633,5 +633,5 @@ function getCheckboxByRowName(name: string) {
}
function renderAccountApp(currentUser: CurrentUser, navigateTo?: string) {
- renderApp('account', routes, { currentUser, navigateTo });
+ renderAppRoutes('account', routes, { currentUser, navigateTo });
}
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx
index 409826aabcf..9000cadeb79 100644
--- a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx
@@ -20,7 +20,7 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AuditLogsServiceMock from '../../../../api/mocks/AuditLogsServiceMock';
-import { renderAdminApp } from '../../../../helpers/testReactTestingUtils';
+import { renderAppWithAdminContext } from '../../../../helpers/testReactTestingUtils';
import { AdminPageExtension } from '../../../../types/extension';
import routes from '../../routes';
@@ -60,5 +60,5 @@ it('should handle download button click', async () => {
});
function renderAuditLogs() {
- renderAdminApp('admin/audit', routes, {}, { adminPages: extensions });
+ renderAppWithAdminContext('admin/audit', routes, {}, { adminPages: extensions });
}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx
index d4d671e4f85..40a58d65802 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx
@@ -22,7 +22,7 @@ import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup';
import ComputeEngineServiceMock from '../../../api/mocks/ComputeEngineServiceMock';
-import { renderAdminApp } from '../../../helpers/testReactTestingUtils';
+import { renderAppWithAdminContext } from '../../../helpers/testReactTestingUtils';
import { TaskStatuses, TaskTypes } from '../../../types/tasks';
import routes from '../routes';
@@ -167,5 +167,5 @@ async function changeTaskFilter(user: UserEvent, fieldLabel: string, value: stri
}
function renderGlobalBackgroundTasksApp() {
- renderAdminApp('admin/background_tasks', routes, {});
+ renderAppWithAdminContext('admin/background_tasks', routes, {});
}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
index 9be7cff423d..9b27956e0c5 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
@@ -18,19 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import AnalysisWarningsModal from '../../../components/common/AnalysisWarningsModal';
import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
import ConfirmModal from '../../../components/controls/ConfirmModal';
-import { lazyLoadComponent } from '../../../components/lazyLoadComponent';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Task, TaskStatuses } from '../../../types/tasks';
import ScannerContext from './ScannerContext';
import Stacktrace from './Stacktrace';
-const AnalysisWarningsModal = lazyLoadComponent(
- () => import('../../../components/common/AnalysisWarningsModal'),
- 'AnalysisWarningsModal'
-);
-
interface Props {
component?: unknown;
onCancelTask: (task: Task) => Promise<void>;
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/TaskActions-test.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/TaskActions-test.tsx
index 714582b3fb1..834a7840b47 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/TaskActions-test.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/TaskActions-test.tsx
@@ -53,10 +53,10 @@ it('shows scanner context', () => {
it('shows warnings', () => {
const wrapper = shallowRender({ warningCount: 2 });
click(wrapper.find('.js-task-show-warnings'));
- expect(wrapper.find('AnalysisWarningsModal')).toMatchSnapshot();
- wrapper.find('AnalysisWarningsModal').prop<Function>('onClose')();
+ expect(wrapper.find('withCurrentUserContext(AnalysisWarningsModal)')).toMatchSnapshot();
+ wrapper.find('withCurrentUserContext(AnalysisWarningsModal)').prop<Function>('onClose')();
wrapper.update();
- expect(wrapper.find('AnalysisWarningsModal').exists()).toBe(false);
+ expect(wrapper.find('withCurrentUserContext(AnalysisWarningsModal)').exists()).toBe(false);
});
function shallowRender(fields?: Partial<Task>, props?: Partial<TaskActions['props']>) {
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap
index ee8b692277e..f526924f1b5 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap
@@ -158,7 +158,7 @@ exports[`shows stack trace 1`] = `
`;
exports[`shows warnings 1`] = `
-<AnalysisWarningsModal
+<withCurrentUserContext(AnalysisWarningsModal)
componentKey="foo"
onClose={[Function]}
taskId="AXR8jg_0mF2ZsYr8Wzs2"
diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
new file mode 100644
index 00000000000..50b2e819f8b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import CodeServiceMock from '../../../api/mocks/CodeServiceMocks';
+import { SourceViewerServiceMock } from '../../../api/mocks/SourceViewerServiceMock';
+import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
+import routes from '../routes';
+
+jest.mock('../../../api/components');
+jest.mock('../../../api/issues');
+
+const handler = new CodeServiceMock();
+const handlerSVM = new SourceViewerServiceMock();
+
+jest.mock('../../../components/SourceViewer/helpers/lines', () => {
+ const lines = jest.requireActual('../../../components/SourceViewer/helpers/lines');
+ return {
+ ...lines,
+ LINES_TO_LOAD: 20
+ };
+});
+
+beforeAll(() => {
+ handlerSVM.reset();
+});
+
+it('should list project files and folder', async () => {
+ renderCode();
+ expect(await screen.findByText('Foo')).toBeInTheDocument();
+ expect(screen.getByText('folderA')).toBeInTheDocument();
+ expect(screen.getByText('index.tsx')).toBeInTheDocument();
+});
+
+it('should dive into folder', async () => {
+ const user = userEvent.setup();
+ renderCode();
+
+ await user.click(await screen.findByText('folderA'));
+ expect(screen.getByText('out.tsx')).toBeInTheDocument();
+});
+
+it('should show source code', async () => {
+ const user = userEvent.setup();
+ renderCode();
+
+ await user.click(await screen.findByText('index.tsx'));
+ expect(screen.getAllByText('function Test() {}')).toHaveLength(20);
+});
+
+function renderCode() {
+ renderAppWithComponentContext('code', routes, {}, { component: handler.getRootComponent() });
+}
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx
deleted file mode 100644
index 19068a28aab..00000000000
--- a/server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { mockComponentMeasure } from '../../../../helpers/mocks/component';
-import { mockMetric } from '../../../../helpers/testMocks';
-import { Component } from '../Component';
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
- expect(shallowRender({ hasBaseComponent: true })).toMatchSnapshot('with base component');
- expect(shallowRender({ isBaseComponent: true })).toMatchSnapshot('is base component');
-});
-
-it('should render correctly for a file', () => {
- expect(shallowRender({ component: mockComponentMeasure(true) })).toMatchSnapshot();
-});
-
-function shallowRender(props: Partial<Component['props']> = {}) {
- return shallow(
- <Component
- component={mockComponentMeasure(false, {
- key: 'bar',
- name: 'Bar',
- measures: [
- { metric: 'bugs', value: '12' },
- { metric: 'vulnerabilities', value: '1' }
- ]
- })}
- hasBaseComponent={false}
- metrics={[mockMetric({ key: 'bugs' }), mockMetric({ key: 'vulnerabilities' })]}
- rootComponent={mockComponentMeasure()}
- {...props}
- />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap
deleted file mode 100644
index d1e4c539e92..00000000000
--- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap
+++ /dev/null
@@ -1,530 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tr
- className=""
->
- <td
- className="blank"
- />
- <td
- className="thin nowrap"
- />
- <td
- className="code-name-cell"
- >
- <div
- className="display-flex-center"
- >
- <ComponentName
- canBrowse={false}
- component={
- Object {
- "key": "bar",
- "measures": Array [
- Object {
- "metric": "bugs",
- "value": "12",
- },
- Object {
- "metric": "vulnerabilities",
- "value": "1",
- },
- ],
- "name": "Bar",
- "qualifier": "TRK",
- }
- }
- rootComponent={
- Object {
- "key": "foo",
- "measures": Array [
- Object {
- "bestValue": false,
- "metric": "bugs",
- "value": "12",
- },
- ],
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- unclickable={false}
- />
- </div>
- </td>
- <td
- className="thin nowrap text-right"
- key="bugs"
- >
- <div
- className="code-components-cell"
- >
- <ComponentMeasure
- component={
- Object {
- "key": "bar",
- "measures": Array [
- Object {
- "metric": "bugs",
- "value": "12",
- },
- Object {
- "metric": "vulnerabilities",
- "value": "1",
- },
- ],
- "name": "Bar",
- "qualifier": "TRK",
- }
- }
- metric={
- Object {
- "id": "bugs",
- "key": "bugs",
- "name": "Bugs",
- "type": "PERCENT",
- }
- }
- />
- </div>
- </td>
- <td
- className="thin nowrap text-right"
- key="vulnerabilities"
- >
- <div
- className="code-components-cell"
- >
- <ComponentMeasure
- component={
- Object {
- "key": "bar",
- "measures": Array [
- Object {
- "metric": "bugs",
- "value": "12",
- },
- Object {
- "metric": "vulnerabilities",
- "value": "1",
- },
- ],
- "name": "Bar",
- "qualifier": "TRK",
- }
- }
- metric={
- Object {
- "id": "vulnerabilities",
- "key": "vulnerabilities",
- "name": "Vulnerabilities",
- "type": "PERCENT",
- }
- }
- />
- </div>
- </td>
- <td
- className="blank"
- />
-</tr>
-`;
-
-exports[`should render correctly for a file 1`] = `
-<tr
- className=""
->
- <td
- className="blank"
- />
- <td
- className="thin nowrap"
- >
- <span
- className="spacer-right"
- >
- <ContextConsumer>
- <Component />
- </ContextConsumer>
- </span>
- </td>
- <td
- className="code-name-cell"
- >
- <div
- className="display-flex-center"
- >
- <ComponentName
- canBrowse={false}
- component={
- Object {
- "key": "foo:src/index.tsx",
- "measures": Array [
- Object {
- "bestValue": false,
- "metric": "bugs",
- "value": "1",
- },
- ],
- "name": "index.tsx",
- "path": "src/index.tsx",
- "qualifier": "FIL",
- }
- }
- rootComponent={
- Object {
- "key": "foo",
- "measures": Array [
- Object {
- "bestValue": false,
- "metric": "bugs",
- "value": "12",
- },
- ],
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- unclickable={false}
- />
- </div>
- </td>
- <td
- className="thin nowrap text-right"
- key="bugs"
- >
- <div
- className="code-components-cell"
- >
- <ComponentMeasure
- component={
- Object {
- "key": "foo:src/index.tsx",
- "measures": Array [
- Object {
- "bestValue": false,
- "metric": "bugs",
- "value": "1",
- },
- ],
- "name": "index.tsx",
- "path": "src/index.tsx",
- "qualifier": "FIL",
- }
- }
- metric={
- Object {
- "id": "bugs",
- "key": "bugs",
- "name": "Bugs",
- "type": "PERCENT",
- }
- }
- />
- </div>
- </td>
- <td
- className="thin nowrap text-right"
- key="vulnerabilities"
- >
- <div
- className="code-components-cell"
- >
- <ComponentMeasure
- component={
- Object {
- "key": "foo:src/index.tsx",
- "measures": Array [
- Object {
- "bestValue": false,
- "metric": "bugs",
- "value": "1",
- },
- ],
- "name": "index.tsx",
- "path": "src/index.tsx",
- "qualifier": "FIL",
- }
- }
- metric={
- Object {
- "id": "vulnerabilities",
- "key": "vulnerabilities",
- "name": "Vulnerabilities",
- "type": "PERCENT",
- }
- }
- />
- </div>
- </td>
- <td
- className="blank"
- />
-</tr>
-`;
-
-exports[`should render correctly: is base component 1`] = `
-<tr
- className=""
->
- <td
- className="blank"
- />
- <td
- className="thin nowrap"
- />
- <td
- className="code-name-cell"
- >
- <div
- className="display-flex-center"
- >
- <ComponentName
- canBrowse={false}
- component={
- Object {
- "key": "bar",
- "measures": Array [
- Object {
- "metric": "bugs",
- "value": "12",
- },
- Object {
- "metric": "vulnerabilities",
- "value": "1",
- },
- ],
- "name": "Bar",
- "qualifier": "TRK",
- }
- }
- rootComponent={
- Object {
- "key": "foo",
- "measures": Array [
- Object {
- "bestValue": false,
- "metric": "bugs",
- "value": "12",
- },
- ],
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- unclickable={true}
- />
- </div>
- </td>
- <td
- className="thin nowrap text-right"
- key="bugs"
- >
- <div
- className="code-components-cell"
- >
- <ComponentMeasure
- component={
- Object {
- "key": "bar",
- "measures": Array [
- Object {
- "metric": "bugs",
- "value": "12",
- },
- Object {
- "metric": "vulnerabilities",
- "value": "1",
- },
- ],
- "name": "Bar",
- "qualifier": "TRK",
- }
- }
- metric={
- Object {
- "id": "bugs",
- "key": "bugs",
- "name": "Bugs",
- "type": "PERCENT",
- }
- }
- />
- </div>
- </td>
- <td
- className="thin nowrap text-right"
- key="vulnerabilities"
- >
- <div
- className="code-components-cell"
- >
- <ComponentMeasure
- component={
- Object {
- "key": "bar",
- "measures": Array [
- Object {
- "metric": "bugs",
- "value": "12",
- },
- Object {
- "metric": "vulnerabilities",
- "value": "1",
- },
- ],
- "name": "Bar",
- "qualifier": "TRK",
- }
- }
- metric={
- Object {
- "id": "vulnerabilities",
- "key": "vulnerabilities",
- "name": "Vulnerabilities",
- "type": "PERCENT",
- }
- }
- />
- </div>
- </td>
- <td
- className="blank"
- />
-</tr>
-`;
-
-exports[`should render correctly: with base component 1`] = `
-<tr
- className=""
->
- <td
- className="blank"
- />
- <td
- className="thin nowrap"
- />
- <td
- className="code-name-cell"
- >
- <div
- className="display-flex-center"
- >
- <div
- className="code-child-component-icon"
- />
- <ComponentName
- canBrowse={false}
- component={
- Object {
- "key": "bar",
- "measures": Array [
- Object {
- "metric": "bugs",
- "value": "12",
- },
- Object {
- "metric": "vulnerabilities",
- "value": "1",
- },
- ],
- "name": "Bar",
- "qualifier": "TRK",
- }
- }
- rootComponent={
- Object {
- "key": "foo",
- "measures": Array [
- Object {
- "bestValue": false,
- "metric": "bugs",
- "value": "12",
- },
- ],
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- unclickable={false}
- />
- </div>
- </td>
- <td
- className="thin nowrap text-right"
- key="bugs"
- >
- <div
- className="code-components-cell"
- >
- <ComponentMeasure
- component={
- Object {
- "key": "bar",
- "measures": Array [
- Object {
- "metric": "bugs",
- "value": "12",
- },
- Object {
- "metric": "vulnerabilities",
- "value": "1",
- },
- ],
- "name": "Bar",
- "qualifier": "TRK",
- }
- }
- metric={
- Object {
- "id": "bugs",
- "key": "bugs",
- "name": "Bugs",
- "type": "PERCENT",
- }
- }
- />
- </div>
- </td>
- <td
- className="thin nowrap text-right"
- key="vulnerabilities"
- >
- <div
- className="code-components-cell"
- >
- <ComponentMeasure
- component={
- Object {
- "key": "bar",
- "measures": Array [
- Object {
- "metric": "bugs",
- "value": "12",
- },
- Object {
- "metric": "vulnerabilities",
- "value": "1",
- },
- ],
- "name": "Bar",
- "qualifier": "TRK",
- }
- }
- metric={
- Object {
- "id": "vulnerabilities",
- "key": "vulnerabilities",
- "name": "Vulnerabilities",
- "type": "PERCENT",
- }
- }
- />
- </div>
- </td>
- <td
- className="blank"
- />
-</tr>
-`;
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
index 4b98d0ba8c8..d723bab2c37 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
+++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
@@ -21,7 +21,7 @@ import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CodingRulesMock from '../../../api/mocks/CodingRulesMock';
import { mockLoggedInUser } from '../../../helpers/testMocks';
-import { renderApp } from '../../../helpers/testReactTestingUtils';
+import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { CurrentUser } from '../../../types/users';
import routes from '../routes';
@@ -479,7 +479,7 @@ it('should show notification for rule advanced section and removes it when user
});
function renderCodingRulesApp(currentUser?: CurrentUser, navigateTo?: string) {
- renderApp('coding_rules', routes, {
+ renderAppRoutes('coding_rules', routes, {
navigateTo,
currentUser,
languages: {
diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/MeasuresApp-it.tsx b/server/sonar-web/src/main/js/apps/component-measures/__tests__/MeasuresApp-it.tsx
index 0d65561407f..e78139b01a7 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/MeasuresApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/MeasuresApp-it.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { screen } from '@testing-library/react';
-import { renderApp } from '../../../helpers/testReactTestingUtils';
+import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import routes from '../routes';
it('should redirect old history route', () => {
@@ -30,5 +30,5 @@ it('should redirect old history route', () => {
});
function renderMeasuresApp(navigateTo?: string) {
- renderApp('component_measures', routes, { navigateTo });
+ renderAppRoutes('component_measures', routes, { navigateTo });
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
index 46fe354db7c..9335ebc7657 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
@@ -97,6 +97,10 @@ exports[`should render correctly for a file 1`] = `
>
<SourceViewer
component="foo:src/index.tsx"
+ displayAllIssues={false}
+ displayIssueLocationsCount={true}
+ displayIssueLocationsLink={true}
+ displayLocationMarkers={true}
metricKey="bugs"
scroll={[Function]}
/>
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap
index a3e7343a950..4d30a972f61 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap
@@ -320,6 +320,10 @@ exports[`should render correctly: is file 1`] = `
>
<SourceViewer
component="foo:src/index.tsx"
+ displayAllIssues={false}
+ displayIssueLocationsCount={true}
+ displayIssueLocationsLink={true}
+ displayLocationMarkers={true}
/>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
index c3184867fb9..b3bf900e1cb 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
@@ -22,9 +22,9 @@ import userEvent from '@testing-library/user-event';
import React from 'react';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
import { renderOwaspTop102021Category } from '../../../helpers/security-standard';
-import { renderApp, renderComponentApp } from '../../../helpers/testReactTestingUtils';
+import { renderApp, renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { IssueType } from '../../../types/issues';
-import AppContainer from '../components/AppContainer';
+import IssuesApp from '../components/IssuesApp';
import { projectIssuesRoutes } from '../routes';
jest.mock('../../../api/issues');
@@ -168,9 +168,9 @@ describe('redirects', () => {
});
function renderIssueApp() {
- renderComponentApp('project/issues', <AppContainer />);
+ renderApp('project/issues', <IssuesApp />);
}
function renderProjectIssuesApp(navigateTo?: string) {
- renderApp('project/issues', projectIssuesRoutes, { navigateTo });
+ renderAppRoutes('project/issues', projectIssuesRoutes, { navigateTo });
}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
deleted file mode 100644
index 04630efd53c..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
-import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation';
-import withIndexationGuard from '../../../components/hoc/withIndexationGuard';
-import { withRouter } from '../../../components/hoc/withRouter';
-import { lazyLoadComponent } from '../../../components/lazyLoadComponent';
-
-const IssuesAppContainer = lazyLoadComponent(() => import('./IssuesApp'), 'IssuesAppContainer');
-
-export default withIndexationGuard(
- withRouter(withCurrentUserContext(withBranchStatusActions(IssuesAppContainer))),
- PageContext.Issues
-);
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
index 99d36dbfc4d..79f773c9221 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
@@ -25,7 +25,10 @@ import { Helmet } from 'react-helmet-async';
import { FormattedMessage } from 'react-intl';
import { searchIssues } from '../../../api/issues';
import { getRuleDetails } from '../../../api/rules';
+import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
import withComponentContext from '../../../app/components/componentContext/withComponentContext';
+import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
+import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import EmptySearch from '../../../components/common/EmptySearch';
import FiltersHeader from '../../../components/common/FiltersHeader';
@@ -36,7 +39,8 @@ import Checkbox from '../../../components/controls/Checkbox';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import ListFooter from '../../../components/controls/ListFooter';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
-import { Location, Router } from '../../../components/hoc/withRouter';
+import withIndexationGuard from '../../../components/hoc/withIndexationGuard';
+import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
import '../../../components/search-navigator.css';
import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
@@ -1162,4 +1166,7 @@ const AlertContent = styled.div`
align-items: center;
`;
-export default withComponentContext(App);
+export default withIndexationGuard(
+ withRouter(withCurrentUserContext(withBranchStatusActions(withComponentContext(App)))),
+ PageContext.Issues
+);
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
index 928f0468e08..b3b93d314f3 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
@@ -17,11 +17,282 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { lazyLoadComponent } from '../../../components/lazyLoadComponent';
+import { findLastIndex, keyBy } from 'lodash';
+import * as React from 'react';
+import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components';
+import { getIssueFlowSnippets } from '../../../api/issues';
+import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
+import {
+ filterDuplicationBlocksByLine,
+ getDuplicationBlocksForIndex,
+ isDuplicationBlockInRemovedComponent
+} from '../../../components/SourceViewer/helpers/duplications';
+import {
+ duplicationsByLine as getDuplicationsByLine,
+ issuesByComponentAndLine
+} from '../../../components/SourceViewer/helpers/indexing';
+import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext';
+import { Alert } from '../../../components/ui/Alert';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { WorkspaceContext } from '../../../components/workspace/context';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { throwGlobalError } from '../../../helpers/error';
+import { translate } from '../../../helpers/l10n';
+import { HttpStatus } from '../../../helpers/request';
+import { BranchLike } from '../../../types/branch-like';
+import { isFile } from '../../../types/component';
+import {
+ Dict,
+ DuplicatedFile,
+ Duplication,
+ FlowLocation,
+ Issue,
+ SnippetsByComponent,
+ SourceViewerFile
+} from '../../../types/types';
+import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer';
+import { getPrimaryLocation, groupLocationsByComponent } from './utils';
-const CrossComponentSourceViewer = lazyLoadComponent(
- () => import(/* webpackPrefetch: true */ './CrossComponentSourceViewerWrapper'),
- 'CrossComponentSourceViewer'
-);
+interface Props {
+ branchLike: BranchLike | undefined;
+ highlightedLocationMessage?: { index: number; text: string | undefined };
+ issue: Issue;
+ issues: Issue[];
+ locations: FlowLocation[];
+ onIssueChange: (issue: Issue) => void;
+ onIssueSelect: (issueKey: string) => void;
+ onLoaded?: () => void;
+ onLocationSelect: (index: number) => void;
+ scroll?: (element: HTMLElement) => void;
+ selectedFlowIndex: number | undefined;
+}
-export default CrossComponentSourceViewer;
+interface State {
+ components: Dict<SnippetsByComponent>;
+ duplicatedFiles?: Dict<DuplicatedFile>;
+ duplications?: Duplication[];
+ duplicationsByLine: { [line: number]: number[] };
+ issuePopup?: { issue: string; name: string };
+ loading: boolean;
+ notAccessible: boolean;
+}
+
+export default class CrossComponentSourceViewer extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {
+ components: {},
+ duplicationsByLine: {},
+ loading: true,
+ notAccessible: false
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchIssueFlowSnippets();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.issue.key !== this.props.issue.key) {
+ this.fetchIssueFlowSnippets();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchDuplications = (component: string) => {
+ getDuplications({
+ key: component,
+ ...getBranchLikeQuery(this.props.branchLike)
+ }).then(
+ r => {
+ if (this.mounted) {
+ this.setState({
+ duplicatedFiles: r.files,
+ duplications: r.duplications,
+ duplicationsByLine: getDuplicationsByLine(r.duplications)
+ });
+ }
+ },
+ () => {
+ /* No error hanlding here */
+ }
+ );
+ };
+
+ async fetchIssueFlowSnippets() {
+ const { issue, branchLike } = this.props;
+ this.setState({ loading: true });
+
+ try {
+ const components = await getIssueFlowSnippets(issue.key);
+ if (components[issue.component] === undefined) {
+ const issueComponent = await getComponentForSourceViewer({
+ component: issue.component,
+ ...getBranchLikeQuery(branchLike)
+ });
+ components[issue.component] = { component: issueComponent, sources: [] };
+ if (isFile(issueComponent.q)) {
+ const sources = await getSources({
+ key: issueComponent.key,
+ ...getBranchLikeQuery(branchLike),
+ from: 1,
+ to: 10
+ }).then(lines => keyBy(lines, 'line'));
+ components[issue.component].sources = sources;
+ }
+ }
+ if (this.mounted) {
+ this.setState({
+ components,
+ issuePopup: undefined,
+ loading: false
+ });
+ if (this.props.onLoaded) {
+ this.props.onLoaded();
+ }
+ }
+ } catch (response) {
+ const rsp = response as Response;
+ if (rsp.status !== HttpStatus.Forbidden) {
+ throwGlobalError(response);
+ }
+ if (this.mounted) {
+ this.setState({ loading: false, notAccessible: rsp.status === HttpStatus.Forbidden });
+ }
+ }
+ }
+
+ handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
+ 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;
+ });
+ };
+
+ renderDuplicationPopup = (component: 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)}
+ openComponent={openComponent}
+ sourceViewerFile={component}
+ />
+ )}
+ </WorkspaceContext.Consumer>
+ );
+ };
+
+ render() {
+ const { loading, notAccessible } = this.state;
+
+ if (loading) {
+ return (
+ <div>
+ <DeferredSpinner />
+ </div>
+ );
+ }
+
+ if (notAccessible) {
+ return (
+ <Alert className="spacer-top" variant="warning">
+ {translate('code_viewer.no_source_code_displayed_due_to_security')}
+ </Alert>
+ );
+ }
+
+ const { issue, locations } = this.props;
+ const { components, duplications, duplicationsByLine } = this.state;
+ const issuesByComponent = issuesByComponentAndLine(this.props.issues);
+ const locationsByComponent = groupLocationsByComponent(issue, locations, components);
+
+ const lastOccurenceOfPrimaryComponent = findLastIndex(
+ locationsByComponent,
+ ({ component }) => component.key === issue.component
+ );
+
+ if (components[issue.component] === undefined) {
+ return null;
+ }
+
+ return (
+ <div>
+ {locationsByComponent.map((snippetGroup, i) => {
+ return (
+ <SourceViewerContext.Provider
+ // eslint-disable-next-line react/no-array-index-key
+ key={`${issue.key}-${this.props.selectedFlowIndex || 0}-${i}`}
+ value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}>
+ <ComponentSourceSnippetGroupViewer
+ branchLike={this.props.branchLike}
+ duplications={duplications}
+ duplicationsByLine={duplicationsByLine}
+ highlightedLocationMessage={this.props.highlightedLocationMessage}
+ issue={issue}
+ issuePopup={this.state.issuePopup}
+ issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
+ isLastOccurenceOfPrimaryComponent={i === lastOccurenceOfPrimaryComponent}
+ lastSnippetGroup={i === locationsByComponent.length - 1}
+ loadDuplications={this.fetchDuplications}
+ locations={snippetGroup.locations || []}
+ onIssueChange={this.props.onIssueChange}
+ onIssueSelect={this.props.onIssueSelect}
+ onIssuePopupToggle={this.handleIssuePopupToggle}
+ onLocationSelect={this.props.onLocationSelect}
+ renderDuplicationPopup={this.renderDuplicationPopup}
+ scroll={this.props.scroll}
+ snippetGroup={snippetGroup}
+ />
+ </SourceViewerContext.Provider>
+ );
+ })}
+
+ {locationsByComponent.length === 0 && (
+ <ComponentSourceSnippetGroupViewer
+ branchLike={this.props.branchLike}
+ duplications={duplications}
+ duplicationsByLine={duplicationsByLine}
+ highlightedLocationMessage={this.props.highlightedLocationMessage}
+ issue={issue}
+ issuePopup={this.state.issuePopup}
+ issuesByLine={issuesByComponent[issue.component] || {}}
+ isLastOccurenceOfPrimaryComponent={true}
+ lastSnippetGroup={true}
+ loadDuplications={this.fetchDuplications}
+ locations={[]}
+ onIssueChange={this.props.onIssueChange}
+ onIssueSelect={this.props.onIssueSelect}
+ onIssuePopupToggle={this.handleIssuePopupToggle}
+ onLocationSelect={this.props.onLocationSelect}
+ renderDuplicationPopup={this.renderDuplicationPopup}
+ scroll={this.props.scroll}
+ snippetGroup={{
+ locations: [getPrimaryLocation(issue)],
+ ...components[issue.component]
+ }}
+ />
+ )}
+ </div>
+ );
+ }
+}
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
deleted file mode 100644
index 11dc8ddab0d..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { findLastIndex, keyBy } from 'lodash';
-import * as React from 'react';
-import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components';
-import { getIssueFlowSnippets } from '../../../api/issues';
-import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
-import {
- filterDuplicationBlocksByLine,
- getDuplicationBlocksForIndex,
- isDuplicationBlockInRemovedComponent
-} from '../../../components/SourceViewer/helpers/duplications';
-import {
- duplicationsByLine as getDuplicationsByLine,
- issuesByComponentAndLine
-} from '../../../components/SourceViewer/helpers/indexing';
-import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext';
-import { Alert } from '../../../components/ui/Alert';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { WorkspaceContext } from '../../../components/workspace/context';
-import { getBranchLikeQuery } from '../../../helpers/branch-like';
-import { throwGlobalError } from '../../../helpers/error';
-import { translate } from '../../../helpers/l10n';
-import { HttpStatus } from '../../../helpers/request';
-import { BranchLike } from '../../../types/branch-like';
-import { isFile } from '../../../types/component';
-import {
- Dict,
- DuplicatedFile,
- Duplication,
- FlowLocation,
- Issue,
- SnippetsByComponent,
- SourceViewerFile
-} from '../../../types/types';
-import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer';
-import { getPrimaryLocation, groupLocationsByComponent } from './utils';
-
-interface Props {
- branchLike: BranchLike | undefined;
- highlightedLocationMessage?: { index: number; text: string | undefined };
- issue: Issue;
- issues: Issue[];
- locations: FlowLocation[];
- onIssueChange: (issue: Issue) => void;
- onIssueSelect: (issueKey: string) => void;
- onLoaded?: () => void;
- onLocationSelect: (index: number) => void;
- scroll?: (element: HTMLElement) => void;
- selectedFlowIndex: number | undefined;
-}
-
-interface State {
- components: Dict<SnippetsByComponent>;
- duplicatedFiles?: Dict<DuplicatedFile>;
- duplications?: Duplication[];
- duplicationsByLine: { [line: number]: number[] };
- issuePopup?: { issue: string; name: string };
- loading: boolean;
- notAccessible: boolean;
-}
-
-export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = {
- components: {},
- duplicationsByLine: {},
- loading: true,
- notAccessible: false
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchIssueFlowSnippets();
- }
-
- componentDidUpdate(prevProps: Props) {
- if (prevProps.issue.key !== this.props.issue.key) {
- this.fetchIssueFlowSnippets();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchDuplications = (component: string) => {
- getDuplications({
- key: component,
- ...getBranchLikeQuery(this.props.branchLike)
- }).then(
- r => {
- if (this.mounted) {
- this.setState({
- duplicatedFiles: r.files,
- duplications: r.duplications,
- duplicationsByLine: getDuplicationsByLine(r.duplications)
- });
- }
- },
- () => {}
- );
- };
-
- async fetchIssueFlowSnippets() {
- const { issue, branchLike } = this.props;
- this.setState({ loading: true });
-
- try {
- const components = await getIssueFlowSnippets(issue.key);
- if (components[issue.component] === undefined) {
- const issueComponent = await getComponentForSourceViewer({
- component: issue.component,
- ...getBranchLikeQuery(branchLike)
- });
- components[issue.component] = { component: issueComponent, sources: [] };
- if (isFile(issueComponent.q)) {
- const sources = await getSources({
- key: issueComponent.key,
- ...getBranchLikeQuery(branchLike),
- from: 1,
- to: 10
- }).then(lines => keyBy(lines, 'line'));
- components[issue.component].sources = sources;
- }
- }
- if (this.mounted) {
- this.setState({
- components,
- issuePopup: undefined,
- loading: false
- });
- if (this.props.onLoaded) {
- this.props.onLoaded();
- }
- }
- } catch (response) {
- const rsp = response as Response;
- if (rsp.status !== HttpStatus.Forbidden) {
- throwGlobalError(response);
- }
- if (this.mounted) {
- this.setState({ loading: false, notAccessible: rsp.status === HttpStatus.Forbidden });
- }
- }
- }
-
- handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
- 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;
- });
- };
-
- renderDuplicationPopup = (component: 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)}
- openComponent={openComponent}
- sourceViewerFile={component}
- />
- )}
- </WorkspaceContext.Consumer>
- );
- };
-
- render() {
- const { loading, notAccessible } = this.state;
-
- if (loading) {
- return (
- <div>
- <DeferredSpinner />
- </div>
- );
- }
-
- if (notAccessible) {
- return (
- <Alert className="spacer-top" variant="warning">
- {translate('code_viewer.no_source_code_displayed_due_to_security')}
- </Alert>
- );
- }
-
- const { issue, locations } = this.props;
- const { components, duplications, duplicationsByLine } = this.state;
- const issuesByComponent = issuesByComponentAndLine(this.props.issues);
- const locationsByComponent = groupLocationsByComponent(issue, locations, components);
-
- const lastOccurenceOfPrimaryComponent = findLastIndex(
- locationsByComponent,
- ({ component }) => component.key === issue.component
- );
-
- if (components[issue.component] === undefined) {
- return null;
- }
-
- return (
- <div>
- {locationsByComponent.map((snippetGroup, i) => {
- return (
- <SourceViewerContext.Provider
- // eslint-disable-next-line react/no-array-index-key
- key={`${issue.key}-${this.props.selectedFlowIndex || 0}-${i}`}
- value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}>
- <ComponentSourceSnippetGroupViewer
- branchLike={this.props.branchLike}
- duplications={duplications}
- duplicationsByLine={duplicationsByLine}
- highlightedLocationMessage={this.props.highlightedLocationMessage}
- issue={issue}
- issuePopup={this.state.issuePopup}
- issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
- isLastOccurenceOfPrimaryComponent={i === lastOccurenceOfPrimaryComponent}
- lastSnippetGroup={i === locationsByComponent.length - 1}
- loadDuplications={this.fetchDuplications}
- locations={snippetGroup.locations || []}
- onIssueChange={this.props.onIssueChange}
- onIssueSelect={this.props.onIssueSelect}
- onIssuePopupToggle={this.handleIssuePopupToggle}
- onLocationSelect={this.props.onLocationSelect}
- renderDuplicationPopup={this.renderDuplicationPopup}
- scroll={this.props.scroll}
- snippetGroup={snippetGroup}
- />
- </SourceViewerContext.Provider>
- );
- })}
-
- {locationsByComponent.length === 0 && (
- <ComponentSourceSnippetGroupViewer
- branchLike={this.props.branchLike}
- duplications={duplications}
- duplicationsByLine={duplicationsByLine}
- highlightedLocationMessage={this.props.highlightedLocationMessage}
- issue={issue}
- issuePopup={this.state.issuePopup}
- issuesByLine={issuesByComponent[issue.component] || {}}
- isLastOccurenceOfPrimaryComponent={true}
- lastSnippetGroup={true}
- loadDuplications={this.fetchDuplications}
- locations={[]}
- onIssueChange={this.props.onIssueChange}
- onIssueSelect={this.props.onIssueSelect}
- onIssuePopupToggle={this.handleIssuePopupToggle}
- onLocationSelect={this.props.onLocationSelect}
- renderDuplicationPopup={this.renderDuplicationPopup}
- scroll={this.props.scroll}
- snippetGroup={{
- locations: [getPrimaryLocation(issue)],
- ...components[issue.component]
- }}
- />
- )}
- </div>
- );
- }
-}
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__/CrossComponentSourceViewer-test.tsx
index 95bb1079de3..9f6472f08e3 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__/CrossComponentSourceViewer-test.tsx
@@ -28,7 +28,7 @@ import {
} from '../../../../helpers/mocks/sources';
import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
-import CrossComponentSourceViewerWrapper from '../CrossComponentSourceViewerWrapper';
+import CrossComponentSourceViewer from '../CrossComponentSourceViewer';
jest.mock('../../../../api/issues', () => {
const { mockSnippetsByComponent } = jest.requireActual('../../../../helpers/mocks/sources');
@@ -122,9 +122,9 @@ it('should handle duplication popup', async () => {
).toMatchSnapshot();
});
-function shallowRender(props: Partial<CrossComponentSourceViewerWrapper['props']> = {}) {
- return shallow<CrossComponentSourceViewerWrapper>(
- <CrossComponentSourceViewerWrapper
+function shallowRender(props: Partial<CrossComponentSourceViewer['props']> = {}) {
+ return shallow<CrossComponentSourceViewer>(
+ <CrossComponentSourceViewer
branchLike={undefined}
highlightedLocationMessage={undefined}
issue={mockIssue(true, { key: '1' })}
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__/CrossComponentSourceViewer-test.tsx.snap
index f587717fbbc..f587717fbbc 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__/CrossComponentSourceViewer-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/issues/routes.tsx b/server/sonar-web/src/main/js/apps/issues/routes.tsx
index ded5c5d5bd3..61354aba355 100644
--- a/server/sonar-web/src/main/js/apps/issues/routes.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/routes.tsx
@@ -21,9 +21,9 @@ import React, { useEffect } from 'react';
import { Route, useNavigate, useSearchParams } from 'react-router-dom';
import { omitNil } from '../../helpers/request';
import { IssueType } from '../../types/issues';
-import AppContainer from './components/AppContainer';
+import IssuesApp from './components/IssuesApp';
-export const globalIssuesRoutes = () => <Route path="issues" element={<AppContainer />} />;
+export const globalIssuesRoutes = () => <Route path="issues" element={<IssuesApp />} />;
export const projectIssuesRoutes = () => (
<Route path="project/issues" element={<IssuesNavigate />} />
@@ -67,5 +67,5 @@ function IssuesNavigate() {
}
}, [navigate, searchParams, setSearchParams]);
- return <AppContainer />;
+ return <IssuesApp />;
}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
index e80ada2ecac..ab37b6e8f59 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
@@ -21,16 +21,11 @@ import tooltipDCE from 'Docs/tooltips/editions/datacenter.md';
import tooltipDE from 'Docs/tooltips/editions/developer.md';
import tooltipEE from 'Docs/tooltips/editions/enterprise.md';
import * as React from 'react';
-import { lazyLoadComponent } from '../../../components/lazyLoadComponent';
+import DocMarkdownBlock from '../../../components/docs/DocMarkdownBlock';
import { getEditionUrl } from '../../../helpers/editions';
import { translate } from '../../../helpers/l10n';
import { Edition, EditionKey } from '../../../types/editions';
-const DocMarkdownBlock = lazyLoadComponent(
- () => import('../../../components/docs/DocMarkdownBlock'),
- 'DocMarkdownBlock'
-);
-
interface Props {
currentEdition?: EditionKey;
edition: Edition;
diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx
index e31cee5531d..b80ca62eabc 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/components/App.tsx
@@ -21,7 +21,6 @@ import * as React from 'react';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import withComponentContext from '../../../app/components/componentContext/withComponentContext';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
-import { lazyLoadComponent } from '../../../components/lazyLoadComponent';
import { isPullRequest } from '../../../helpers/branch-like';
import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
import { AppState } from '../../../types/appstate';
@@ -29,9 +28,8 @@ import { BranchLike } from '../../../types/branch-like';
import { isPortfolioLike } from '../../../types/component';
import { Component } from '../../../types/types';
import BranchOverview from '../branches/BranchOverview';
-
-const EmptyOverview = lazyLoadComponent(() => import('./EmptyOverview'));
-const PullRequestOverview = lazyLoadComponent(() => import('../pullRequests/PullRequestOverview'));
+import PullRequestOverview from '../pullRequests/PullRequestOverview';
+import EmptyOverview from './EmptyOverview';
interface Props {
appState: AppState;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx
index c981572da89..06306de2aee 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx
@@ -22,7 +22,7 @@ import React from 'react';
import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext';
import { getActivityGraph } from '../../../../components/activity-graph/utils';
import { mockComponent } from '../../../../helpers/mocks/component';
-import { renderComponentApp } from '../../../../helpers/testReactTestingUtils';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { ComponentQualifier } from '../../../../types/component';
import { Component } from '../../../../types/types';
import ProjectActivityAppContainer from '../ProjectActivityAppContainer';
@@ -104,7 +104,7 @@ function renderProjectActivityAppContainer(
})
}
) {
- return renderComponentApp(
+ return renderApp(
'project/activity',
<ComponentContext.Provider
value={{
diff --git a/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx
index aad3a0b30d7..753f5872bdf 100644
--- a/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx
@@ -21,7 +21,7 @@ import { screen } from '@testing-library/react';
import * as React from 'react';
import { ComponentContext } from '../../../app/components/componentContext/ComponentContext';
import { mockComponent } from '../../../helpers/mocks/component';
-import { renderComponentApp } from '../../../helpers/testReactTestingUtils';
+import { renderApp } from '../../../helpers/testReactTestingUtils';
import { ComponentContextShape } from '../../../types/component';
import { Component } from '../../../types/types';
import App from '../App';
@@ -39,7 +39,7 @@ it('should render with no component', () => {
});
function renderProjectDeletionApp(component?: Component) {
- renderComponentApp(
+ renderApp(
'project-delete',
<ComponentContext.Provider value={{ component } as ComponentContextShape}>
<App />
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx
deleted file mode 100644
index 01d581d97a7..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { lazyLoadComponent } from '../../../components/lazyLoadComponent';
-
-export default lazyLoadComponent(() => import('./AllProjects'));
diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
index fa938e1a828..97ed1257bff 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
@@ -26,7 +26,7 @@ import { get } from '../../../helpers/storage';
import { hasGlobalPermission } from '../../../helpers/users';
import { CurrentUser, isLoggedIn } from '../../../types/users';
import { PROJECTS_ALL, PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE } from '../utils';
-import AllProjectsContainer from './AllProjectsContainer';
+import AllProjects from './AllProjects';
export interface DefaultPageSelectorProps {
currentUser: CurrentUser;
@@ -95,7 +95,7 @@ export function DefaultPageSelector(props: DefaultPageSelectorProps) {
return null;
}
- return <AllProjectsContainer isFavorite={false} />;
+ return <AllProjects isFavorite={false} />;
}
export default withCurrentUserContext(DefaultPageSelector);
diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx
index 8d5bbad6d56..aad6fbcec4e 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx
@@ -18,8 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import AllProjectsContainer from './AllProjectsContainer';
+import AllProjects from './AllProjects';
export default function FavoriteProjectsContainer(props: any) {
- return <AllProjectsContainer isFavorite={true} {...props} />;
+ return <AllProjects isFavorite={true} {...props} />;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx
index 2c35ecc0571..8f1a8f5533d 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx
@@ -29,10 +29,10 @@ import { CurrentUser } from '../../../../types/users';
import { DefaultPageSelector } from '../DefaultPageSelector';
jest.mock(
- '../AllProjectsContainer',
+ '../AllProjects',
() =>
// eslint-disable-next-line
- function AllProjectsContainer() {
+ function AllProjects() {
return <div>All Projects</div>;
}
);
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
index 36bbd9969e6..7c7aa6f02bd 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
@@ -22,7 +22,7 @@ import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getComponents, SearchProjectsParameters } from '../../../api/components';
import PermissionTemplateServiceMock from '../../../api/mocks/PermissionTemplateServiceMock';
-import { renderAdminApp } from '../../../helpers/testReactTestingUtils';
+import { renderAppWithAdminContext } from '../../../helpers/testReactTestingUtils';
import { ComponentQualifier, Visibility } from '../../../types/component';
import routes from '../routes';
@@ -154,5 +154,5 @@ function mockComponents(n: number) {
}
function renderGlobalBackgroundTasksApp() {
- renderAdminApp('admin/projects_management', routes, {});
+ renderAppWithAdminContext('admin/projects_management', routes, {});
}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx
index 46ee39cc0cc..be1520ef628 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx
@@ -23,7 +23,7 @@ import selectEvent from 'react-select-event';
import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
import { searchProjects, searchUsers } from '../../../../api/quality-gates';
import { mockAppState } from '../../../../helpers/testMocks';
-import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
import { AppState } from '../../../../types/appstate';
import routes from '../../routes';
@@ -488,5 +488,5 @@ describe('The Permissions section', () => {
});
function renderQualityGateApp(appState?: AppState) {
- renderApp('quality_gates', routes, { appState });
+ renderAppRoutes('quality_gates', routes, { appState });
}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx
index f379df82a5d..7b400b19a60 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx
@@ -21,15 +21,10 @@ import * as React from 'react';
import { Profile } from '../../../api/quality-profiles';
import { getRuleDetails } from '../../../api/rules';
import { Button } from '../../../components/controls/buttons';
-import { lazyLoadComponent } from '../../../components/lazyLoadComponent';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { translate } from '../../../helpers/l10n';
import { RuleDetails } from '../../../types/types';
-
-const ActivationFormModal = lazyLoadComponent(
- () => import('../../coding-rules/components/ActivationFormModal'),
- 'ActivationFormModal'
-);
+import ActivationFormModal from '../../coding-rules/components/ActivationFormModal';
interface Props {
onDone: () => Promise<void>;
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 87d95775bdf..3c43b8ce50b 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
@@ -17,10 +17,651 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { lazyLoadComponent } from '../lazyLoadComponent';
+import { intersection, uniqBy } from 'lodash';
+import * as React from 'react';
+import {
+ getComponentData,
+ getComponentForSourceViewer,
+ getDuplications,
+ getSources
+} from '../../api/components';
+import { getBranchLikeQuery, isSameBranchLike } from '../../helpers/branch-like';
+import { translate } from '../../helpers/l10n';
+import { HttpStatus } from '../../helpers/request';
+import { BranchLike } from '../../types/branch-like';
+import {
+ Dict,
+ DuplicatedFile,
+ Duplication,
+ FlowLocation,
+ Issue,
+ LinearIssueLocation,
+ Measure,
+ SourceLine,
+ SourceViewerFile
+} from '../../types/types';
+import { Alert } from '../ui/Alert';
+import { WorkspaceContext } from '../workspace/context';
+import DuplicationPopup from './components/DuplicationPopup';
+import {
+ filterDuplicationBlocksByLine,
+ getDuplicationBlocksForIndex,
+ isDuplicationBlockInRemovedComponent
+} from './helpers/duplications';
+import getCoverageStatus from './helpers/getCoverageStatus';
+import {
+ duplicationsByLine,
+ issuesByLine,
+ locationsByLine,
+ symbolsByLine
+} from './helpers/indexing';
+import { LINES_TO_LOAD } from './helpers/lines';
+import defaultLoadIssues from './helpers/loadIssues';
+import SourceViewerCode from './SourceViewerCode';
+import { SourceViewerContext } from './SourceViewerContext';
+import SourceViewerHeader from './SourceViewerHeader';
+import SourceViewerHeaderSlim from './SourceViewerHeaderSlim';
+import './styles.css';
-const SourceViewer = lazyLoadComponent(
- () => import(/* webpackPrefetch: true */ './SourceViewerBase'),
- 'SourceViewer'
-);
-export default SourceViewer;
+export interface Props {
+ aroundLine?: number;
+ branchLike: BranchLike | undefined;
+ component: string;
+ componentMeasures?: Measure[];
+ displayAllIssues?: boolean;
+ displayIssueLocationsCount?: boolean;
+ displayIssueLocationsLink?: boolean;
+ displayLocationMarkers?: boolean;
+ highlightedLine?: number;
+ // `undefined` elements mean they are located in a different file,
+ // but kept to maintaint the location indexes
+ highlightedLocations?: (FlowLocation | undefined)[];
+ highlightedLocationMessage?: { index: number; text: string | undefined };
+ loadIssues?: (
+ component: string,
+ from: number,
+ to: number,
+ branchLike: BranchLike | undefined
+ ) => Promise<Issue[]>;
+ onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
+ onLocationSelect?: (index: number) => void;
+ onIssueChange?: (issue: Issue) => void;
+ onIssueSelect?: (issueKey: string) => void;
+ onIssueUnselect?: () => void;
+ scroll?: (element: HTMLElement) => void;
+ selectedIssue?: string;
+ showMeasures?: boolean;
+ metricKey?: string;
+ slimHeader?: boolean;
+}
+
+interface State {
+ component?: SourceViewerFile;
+ duplicatedFiles?: Dict<DuplicatedFile>;
+ duplications?: Duplication[];
+ duplicationsByLine: { [line: number]: number[] };
+ hasSourcesAfter: boolean;
+ highlightedSymbols: string[];
+ issueLocationsByLine: { [line: number]: LinearIssueLocation[] };
+ issuePopup?: { issue: string; name: string };
+ issues?: Issue[];
+ issuesByLine: { [line: number]: Issue[] };
+ loading: boolean;
+ loadingSourcesAfter: boolean;
+ loadingSourcesBefore: boolean;
+ notAccessible: boolean;
+ notExist: boolean;
+ openIssuesByLine: { [line: number]: boolean };
+ selectedIssue?: string;
+ sourceRemoved: boolean;
+ sources?: SourceLine[];
+ symbolsByLine: { [line: number]: string[] };
+}
+
+export default class SourceViewer extends React.PureComponent<Props, State> {
+ node?: HTMLElement | null;
+ mounted = false;
+
+ static defaultProps = {
+ displayAllIssues: false,
+ displayIssueLocationsCount: true,
+ displayIssueLocationsLink: true,
+ displayLocationMarkers: true
+ };
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ duplicationsByLine: {},
+ hasSourcesAfter: false,
+ highlightedSymbols: [],
+ issuesByLine: {},
+ issueLocationsByLine: {},
+ loading: true,
+ loadingSourcesAfter: false,
+ loadingSourcesBefore: false,
+ notAccessible: false,
+ notExist: false,
+ openIssuesByLine: {},
+ selectedIssue: props.selectedIssue,
+ sourceRemoved: false,
+ symbolsByLine: {}
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchComponent();
+ }
+
+ async componentDidUpdate(prevProps: Props) {
+ if (
+ this.props.onIssueSelect !== undefined &&
+ this.props.selectedIssue !== prevProps.selectedIssue
+ ) {
+ this.setState({ selectedIssue: this.props.selectedIssue });
+ }
+ if (
+ prevProps.component !== this.props.component ||
+ !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
+ ) {
+ this.fetchComponent();
+ } else if (
+ this.props.aroundLine !== undefined &&
+ prevProps.aroundLine !== this.props.aroundLine &&
+ 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),
+ hasSourcesAfter: sources.length > LINES_TO_LOAD
+ },
+ () => {
+ if (this.props.onLoaded && this.state.component && this.state.issues) {
+ this.props.onLoaded(this.state.component, finalSources, this.state.issues);
+ }
+ }
+ );
+ }
+ } else {
+ this.checkSelectedIssueChange();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadComponent(component: string, branchLike?: BranchLike) {
+ return Promise.all([
+ getComponentForSourceViewer({ component, ...getBranchLikeQuery(branchLike) }),
+ getComponentData({ component, ...getBranchLikeQuery(branchLike) })
+ ]).then(([sourceViewerComponent, { component }]) => ({
+ ...sourceViewerComponent,
+ leakPeriodDate: component.leakPeriodDate
+ }));
+ }
+
+ checkSelectedIssueChange() {
+ const { selectedIssue } = this.props;
+ const { issues } = this.state;
+ if (
+ selectedIssue !== undefined &&
+ issues !== undefined &&
+ issues.find(issue => issue.key === selectedIssue) === undefined
+ ) {
+ this.reloadIssues();
+ }
+ }
+
+ loadSources(
+ key: string,
+ from: number | undefined,
+ to: number | undefined,
+ branchLike: BranchLike | undefined
+ ) {
+ return getSources({ key, from, to, ...getBranchLikeQuery(branchLike) });
+ }
+
+ get loadIssues() {
+ return this.props.loadIssues || defaultLoadIssues;
+ }
+
+ computeCoverageStatus(lines: SourceLine[]) {
+ return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) }));
+ }
+
+ isLineOutsideOfRange(lineNumber: number) {
+ const { sources } = this.state;
+ if (sources && sources.length > 0) {
+ const firstLine = sources[0];
+ const lastList = sources[sources.length - 1];
+ return lineNumber < firstLine.line || lineNumber > lastList.line;
+ }
+
+ return true;
+ }
+
+ fetchComponent() {
+ this.setState({ loading: true });
+
+ const to = (this.props.aroundLine || 0) + LINES_TO_LOAD;
+ const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => {
+ this.loadIssues(this.props.component, 1, to, this.props.branchLike).then(
+ issues => {
+ if (this.mounted) {
+ const finalSources = sources.slice(0, LINES_TO_LOAD);
+ this.setState(
+ {
+ component,
+ duplicatedFiles: undefined,
+ duplications: undefined,
+ duplicationsByLine: {},
+ hasSourcesAfter: sources.length > LINES_TO_LOAD,
+ highlightedSymbols: [],
+ issueLocationsByLine: locationsByLine(issues),
+ 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))
+ },
+ () => {
+ if (this.props.onLoaded) {
+ this.props.onLoaded(component, finalSources, issues);
+ }
+ }
+ );
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ };
+
+ const onFailLoadComponent = (response: Response) => {
+ if (this.mounted) {
+ if (response.status === HttpStatus.Forbidden) {
+ this.setState({ loading: false, notAccessible: true });
+ } else if (response.status === HttpStatus.NotFound) {
+ this.setState({ loading: false, notExist: true });
+ }
+ }
+ };
+
+ const onFailLoadSources = (response: Response, component: SourceViewerFile) => {
+ if (this.mounted) {
+ if (response.status === HttpStatus.Forbidden) {
+ this.setState({ component, loading: false, notAccessible: true });
+ } else if (response.status === HttpStatus.NotFound) {
+ this.setState({ component, loading: false, sourceRemoved: true });
+ }
+ }
+ };
+
+ const onResolve = (component: SourceViewerFile) => {
+ const sourcesRequest =
+ component.q === 'FIL' || component.q === 'UTS' ? this.fetchSources() : Promise.resolve([]);
+ sourcesRequest.then(
+ sources => loadIssues(component, sources),
+ response => onFailLoadSources(response, component)
+ );
+ };
+
+ this.loadComponent(this.props.component, this.props.branchLike).then(
+ onResolve,
+ onFailLoadComponent
+ );
+ }
+
+ reloadIssues() {
+ if (!this.state.sources) {
+ return;
+ }
+ const firstSourceLine = this.state.sources[0];
+ const lastSourceLine = this.state.sources[this.state.sources.length - 1];
+ this.loadIssues(
+ this.props.component,
+ firstSourceLine && firstSourceLine.line,
+ lastSourceLine && lastSourceLine.line,
+ this.props.branchLike
+ ).then(
+ issues => {
+ if (this.mounted) {
+ this.setState({
+ issues,
+ issuesByLine: issuesByLine(issues),
+ issueLocationsByLine: locationsByLine(issues)
+ });
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ }
+
+ fetchSources = (): Promise<SourceLine[]> => {
+ return new Promise((resolve, reject) => {
+ const onFailLoadSources = (response: Response) => {
+ if (this.mounted) {
+ if ([HttpStatus.Forbidden, HttpStatus.NotFound].includes(response.status)) {
+ reject(response);
+ } else {
+ resolve([]);
+ }
+ }
+ };
+
+ const from = this.props.aroundLine
+ ? Math.max(1, this.props.aroundLine - LINES_TO_LOAD / 2 + 1)
+ : 1;
+
+ 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++;
+
+ this.loadSources(this.props.component, from, to, this.props.branchLike).then(sources => {
+ resolve(sources);
+ }, onFailLoadSources);
+ });
+ };
+
+ loadSourcesBefore = () => {
+ 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);
+ Promise.all([
+ this.loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike),
+ this.loadIssues(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike)
+ ]).then(
+ ([sources, issues]) => {
+ if (this.mounted) {
+ this.setState(prevState => {
+ const nextIssues = uniqBy([...issues, ...(prevState.issues || [])], issue => issue.key);
+ return {
+ issues: nextIssues,
+ issuesByLine: issuesByLine(nextIssues),
+ issueLocationsByLine: locationsByLine(nextIssues),
+ loadingSourcesBefore: false,
+ sources: [...this.computeCoverageStatus(sources), ...(prevState.sources || [])],
+ symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) }
+ };
+ });
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ };
+
+ loadSourcesAfter = () => {
+ if (!this.state.sources) {
+ return;
+ }
+ const lastSourceLine = this.state.sources[this.state.sources.length - 1];
+ this.setState({ loadingSourcesAfter: true });
+ const fromLine = lastSourceLine.line + 1;
+ // request one additional line to define `hasSourcesAfter`
+ const toLine = lastSourceLine.line + LINES_TO_LOAD + 1;
+ Promise.all([
+ this.loadSources(this.props.component, fromLine, toLine, this.props.branchLike),
+ this.loadIssues(this.props.component, fromLine, toLine, this.props.branchLike)
+ ]).then(
+ ([sources, issues]) => {
+ if (this.mounted) {
+ this.setState(prevState => {
+ const nextIssues = uniqBy([...(prevState.issues || []), ...issues], issue => issue.key);
+ return {
+ issues: nextIssues,
+ issuesByLine: issuesByLine(nextIssues),
+ issueLocationsByLine: locationsByLine(nextIssues),
+ hasSourcesAfter: sources.length > LINES_TO_LOAD,
+ loadingSourcesAfter: false,
+ sources: [
+ ...(prevState.sources || []),
+ ...this.computeCoverageStatus(sources.slice(0, LINES_TO_LOAD))
+ ],
+ symbolsByLine: {
+ ...prevState.symbolsByLine,
+ ...symbolsByLine(sources.slice(0, LINES_TO_LOAD))
+ }
+ };
+ });
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ };
+
+ loadDuplications = () => {
+ getDuplications({
+ key: this.props.component,
+ ...getBranchLikeQuery(this.props.branchLike)
+ }).then(
+ r => {
+ if (this.mounted) {
+ this.setState({
+ duplications: r.duplications,
+ duplicationsByLine: duplicationsByLine(r.duplications),
+ duplicatedFiles: r.files
+ });
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ };
+
+ handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
+ 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;
+ });
+ };
+
+ handleSymbolClick = (symbols: string[]) => {
+ this.setState(state => {
+ const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0;
+ const highlightedSymbols = shouldDisable ? [] : symbols;
+ return { highlightedSymbols };
+ });
+ };
+
+ handleIssueSelect = (issue: string) => {
+ if (this.props.onIssueSelect) {
+ this.props.onIssueSelect(issue);
+ } else {
+ this.setState({ selectedIssue: issue });
+ }
+ };
+
+ handleIssueUnselect = () => {
+ if (this.props.onIssueUnselect) {
+ this.props.onIssueUnselect();
+ } else {
+ this.setState({ selectedIssue: undefined });
+ }
+ };
+
+ handleOpenIssues = (line: SourceLine) => {
+ this.setState(state => ({
+ openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
+ }));
+ };
+
+ handleCloseIssues = (line: SourceLine) => {
+ this.setState(state => ({
+ openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
+ }));
+ };
+
+ handleIssueChange = (issue: Issue) => {
+ this.setState(({ issues = [] }) => {
+ const newIssues = issues.map(candidate => (candidate.key === issue.key ? issue : candidate));
+ return { issues: newIssues, issuesByLine: issuesByLine(newIssues) };
+ });
+ if (this.props.onIssueChange) {
+ this.props.onIssueChange(issue);
+ }
+ };
+
+ renderDuplicationPopup = (index: number, line: number) => {
+ const { component, 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)}
+ openComponent={openComponent}
+ sourceViewerFile={component}
+ />
+ )}
+ </WorkspaceContext.Consumer>
+ );
+ };
+
+ renderCode(sources: SourceLine[]) {
+ const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
+ return (
+ <SourceViewerCode
+ branchLike={this.props.branchLike}
+ displayAllIssues={this.props.displayAllIssues}
+ displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+ displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+ displayLocationMarkers={this.props.displayLocationMarkers}
+ duplications={this.state.duplications}
+ duplicationsByLine={this.state.duplicationsByLine}
+ hasSourcesAfter={this.state.hasSourcesAfter}
+ hasSourcesBefore={hasSourcesBefore}
+ highlightedLine={this.props.highlightedLine}
+ highlightedLocationMessage={this.props.highlightedLocationMessage}
+ highlightedLocations={this.props.highlightedLocations}
+ highlightedSymbols={this.state.highlightedSymbols}
+ issueLocationsByLine={this.state.issueLocationsByLine}
+ issuePopup={this.state.issuePopup}
+ 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}
+ onIssueChange={this.handleIssueChange}
+ onIssuePopupToggle={this.handleIssuePopupToggle}
+ onIssueSelect={this.handleIssueSelect}
+ onIssueUnselect={this.handleIssueUnselect}
+ onIssuesClose={this.handleCloseIssues}
+ onIssuesOpen={this.handleOpenIssues}
+ onLocationSelect={this.props.onLocationSelect}
+ onSymbolClick={this.handleSymbolClick}
+ openIssuesByLine={this.state.openIssuesByLine}
+ renderDuplicationPopup={this.renderDuplicationPopup}
+ scroll={this.props.scroll}
+ metricKey={this.props.metricKey}
+ selectedIssue={this.state.selectedIssue}
+ sources={sources}
+ symbolsByLine={this.state.symbolsByLine}
+ />
+ );
+ }
+
+ renderHeader(branchLike: BranchLike | undefined, sourceViewerFile: SourceViewerFile) {
+ return this.props.slimHeader ? (
+ <SourceViewerHeaderSlim branchLike={branchLike} sourceViewerFile={sourceViewerFile} />
+ ) : (
+ <WorkspaceContext.Consumer>
+ {({ openComponent }) => (
+ <SourceViewerHeader
+ branchLike={this.props.branchLike}
+ componentMeasures={this.props.componentMeasures}
+ openComponent={openComponent}
+ showMeasures={this.props.showMeasures}
+ sourceViewerFile={sourceViewerFile}
+ />
+ )}
+ </WorkspaceContext.Consumer>
+ );
+ }
+
+ render() {
+ const { component, loading, sources, notAccessible, sourceRemoved } = this.state;
+
+ if (loading) {
+ return null;
+ }
+
+ if (this.state.notExist) {
+ return (
+ <Alert className="spacer-top" variant="warning">
+ {translate('component_viewer.no_component')}
+ </Alert>
+ );
+ }
+
+ if (notAccessible) {
+ return (
+ <Alert className="spacer-top" variant="warning">
+ {translate('code_viewer.no_source_code_displayed_due_to_security')}
+ </Alert>
+ );
+ }
+
+ if (!component) {
+ return null;
+ }
+
+ return (
+ <SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
+ <div className="source-viewer" ref={node => (this.node = node)}>
+ {this.renderHeader(this.props.branchLike, 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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
deleted file mode 100644
index 93950d7fff7..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
+++ /dev/null
@@ -1,673 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { intersection, uniqBy } from 'lodash';
-import * as React from 'react';
-import {
- getComponentData,
- getComponentForSourceViewer,
- getDuplications,
- getSources
-} from '../../api/components';
-import { Alert } from '../../components/ui/Alert';
-import { getBranchLikeQuery, isSameBranchLike } from '../../helpers/branch-like';
-import { translate } from '../../helpers/l10n';
-import { BranchLike } from '../../types/branch-like';
-import {
- Dict,
- DuplicatedFile,
- Duplication,
- FlowLocation,
- Issue,
- LinearIssueLocation,
- Measure,
- SourceLine,
- SourceViewerFile
-} from '../../types/types';
-import { WorkspaceContext } from '../workspace/context';
-import DuplicationPopup from './components/DuplicationPopup';
-import {
- filterDuplicationBlocksByLine,
- getDuplicationBlocksForIndex,
- isDuplicationBlockInRemovedComponent
-} from './helpers/duplications';
-import getCoverageStatus from './helpers/getCoverageStatus';
-import {
- duplicationsByLine,
- issuesByLine,
- locationsByLine,
- symbolsByLine
-} from './helpers/indexing';
-import { LINES_TO_LOAD } from './helpers/lines';
-import defaultLoadIssues from './helpers/loadIssues';
-import SourceViewerCode from './SourceViewerCode';
-import { SourceViewerContext } from './SourceViewerContext';
-import SourceViewerHeader from './SourceViewerHeader';
-import SourceViewerHeaderSlim from './SourceViewerHeaderSlim';
-import './styles.css';
-
-// TODO react-virtualized
-
-export interface Props {
- aroundLine?: number;
- branchLike: BranchLike | undefined;
- component: string;
- componentMeasures?: Measure[];
- displayAllIssues?: boolean;
- displayIssueLocationsCount?: boolean;
- displayIssueLocationsLink?: boolean;
- displayLocationMarkers?: boolean;
- highlightedLine?: number;
- // `undefined` elements mean they are located in a different file,
- // but kept to maintaint the location indexes
- highlightedLocations?: (FlowLocation | undefined)[];
- highlightedLocationMessage?: { index: number; text: string | undefined };
- loadIssues?: (
- component: string,
- from: number,
- to: number,
- branchLike: BranchLike | undefined
- ) => Promise<Issue[]>;
- onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
- onLocationSelect?: (index: number) => void;
- onIssueChange?: (issue: Issue) => void;
- onIssueSelect?: (issueKey: string) => void;
- onIssueUnselect?: () => void;
- scroll?: (element: HTMLElement) => void;
- selectedIssue?: string;
- showMeasures?: boolean;
- metricKey?: string;
- slimHeader?: boolean;
-}
-
-interface State {
- component?: SourceViewerFile;
- duplicatedFiles?: Dict<DuplicatedFile>;
- duplications?: Duplication[];
- duplicationsByLine: { [line: number]: number[] };
- hasSourcesAfter: boolean;
- highlightedSymbols: string[];
- issueLocationsByLine: { [line: number]: LinearIssueLocation[] };
- issuePopup?: { issue: string; name: string };
- issues?: Issue[];
- issuesByLine: { [line: number]: Issue[] };
- loading: boolean;
- loadingSourcesAfter: boolean;
- loadingSourcesBefore: boolean;
- notAccessible: boolean;
- notExist: boolean;
- openIssuesByLine: { [line: number]: boolean };
- selectedIssue?: string;
- sourceRemoved: boolean;
- sources?: SourceLine[];
- symbolsByLine: { [line: number]: string[] };
-}
-
-export default class SourceViewerBase extends React.PureComponent<Props, State> {
- node?: HTMLElement | null;
- mounted = false;
-
- static defaultProps = {
- displayAllIssues: false,
- displayIssueLocationsCount: true,
- displayIssueLocationsLink: true,
- displayLocationMarkers: true
- };
-
- constructor(props: Props) {
- super(props);
-
- this.state = {
- duplicationsByLine: {},
- hasSourcesAfter: false,
- highlightedSymbols: [],
- issuesByLine: {},
- issueLocationsByLine: {},
- loading: true,
- loadingSourcesAfter: false,
- loadingSourcesBefore: false,
- notAccessible: false,
- notExist: false,
- openIssuesByLine: {},
- selectedIssue: props.selectedIssue,
- sourceRemoved: false,
- symbolsByLine: {}
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- this.fetchComponent();
- }
-
- componentDidUpdate(prevProps: Props) {
- if (
- this.props.onIssueSelect !== undefined &&
- this.props.selectedIssue !== prevProps.selectedIssue
- ) {
- this.setState({ selectedIssue: this.props.selectedIssue });
- }
- if (
- prevProps.component !== this.props.component ||
- !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
- ) {
- this.fetchComponent();
- } else if (
- this.props.aroundLine !== undefined &&
- prevProps.aroundLine !== this.props.aroundLine &&
- this.isLineOutsideOfRange(this.props.aroundLine)
- ) {
- this.fetchSources().then(
- sources => {
- if (this.mounted) {
- const finalSources = sources.slice(0, LINES_TO_LOAD);
- this.setState(
- {
- sources: sources.slice(0, LINES_TO_LOAD),
- hasSourcesAfter: sources.length > LINES_TO_LOAD
- },
- () => {
- if (this.props.onLoaded && this.state.component && this.state.issues) {
- this.props.onLoaded(this.state.component, finalSources, this.state.issues);
- }
- }
- );
- }
- },
- () => {
- // TODO
- }
- );
- } else {
- const { selectedIssue } = this.props;
- const { issues } = this.state;
- if (
- selectedIssue !== undefined &&
- issues !== undefined &&
- issues.find(issue => issue.key === selectedIssue) === undefined
- ) {
- this.reloadIssues();
- }
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- loadComponent(component: string, branchLike?: BranchLike) {
- return Promise.all([
- getComponentForSourceViewer({ component, ...getBranchLikeQuery(branchLike) }),
- getComponentData({ component, ...getBranchLikeQuery(branchLike) })
- ]).then(([sourceViewerComponent, { component }]) => ({
- ...sourceViewerComponent,
- leakPeriodDate: component.leakPeriodDate
- }));
- }
-
- loadSources(
- key: string,
- from: number | undefined,
- to: number | undefined,
- branchLike: BranchLike | undefined
- ) {
- return getSources({ key, from, to, ...getBranchLikeQuery(branchLike) });
- }
-
- get loadIssues() {
- return this.props.loadIssues || defaultLoadIssues;
- }
-
- computeCoverageStatus(lines: SourceLine[]) {
- return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) }));
- }
-
- isLineOutsideOfRange(lineNumber: number) {
- const { sources } = this.state;
- if (sources && sources.length > 0) {
- const firstLine = sources[0];
- const lastList = sources[sources.length - 1];
- return lineNumber < firstLine.line || lineNumber > lastList.line;
- }
-
- return true;
- }
-
- fetchComponent() {
- this.setState({ loading: true });
-
- const to = (this.props.aroundLine || 0) + LINES_TO_LOAD;
- const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => {
- this.loadIssues(this.props.component, 1, to, this.props.branchLike).then(
- issues => {
- if (this.mounted) {
- const finalSources = sources.slice(0, LINES_TO_LOAD);
- this.setState(
- {
- component,
- duplicatedFiles: undefined,
- duplications: undefined,
- duplicationsByLine: {},
- hasSourcesAfter: sources.length > LINES_TO_LOAD,
- highlightedSymbols: [],
- issueLocationsByLine: locationsByLine(issues),
- 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))
- },
- () => {
- if (this.props.onLoaded) {
- this.props.onLoaded(component, finalSources, issues);
- }
- }
- );
- }
- },
- () => {
- // TODO
- }
- );
- };
-
- const onFailLoadComponent = (response: Response) => {
- // TODO handle other statuses
- if (this.mounted) {
- if (response.status === 403) {
- this.setState({ loading: false, notAccessible: true });
- } else if (response.status === 404) {
- this.setState({ loading: false, notExist: true });
- }
- }
- };
-
- const onFailLoadSources = (response: Response, component: SourceViewerFile) => {
- // TODO handle other statuses
- if (this.mounted) {
- if (response.status === 403) {
- this.setState({ component, loading: false, notAccessible: true });
- } else if (response.status === 404) {
- this.setState({ component, loading: false, sourceRemoved: true });
- }
- }
- };
-
- const onResolve = (component: SourceViewerFile) => {
- const sourcesRequest =
- component.q === 'FIL' || component.q === 'UTS' ? this.fetchSources() : Promise.resolve([]);
- sourcesRequest.then(
- sources => loadIssues(component, sources),
- response => onFailLoadSources(response, component)
- );
- };
-
- this.loadComponent(this.props.component, this.props.branchLike).then(
- onResolve,
- onFailLoadComponent
- );
- }
-
- reloadIssues() {
- if (!this.state.sources) {
- return;
- }
- const firstSourceLine = this.state.sources[0];
- const lastSourceLine = this.state.sources[this.state.sources.length - 1];
- this.loadIssues(
- this.props.component,
- firstSourceLine && firstSourceLine.line,
- lastSourceLine && lastSourceLine.line,
- this.props.branchLike
- ).then(
- issues => {
- if (this.mounted) {
- this.setState({
- issues,
- issuesByLine: issuesByLine(issues),
- issueLocationsByLine: locationsByLine(issues)
- });
- }
- },
- () => {
- // TODO
- }
- );
- }
-
- fetchSources = (): Promise<SourceLine[]> => {
- return new Promise((resolve, reject) => {
- const onFailLoadSources = (response: Response) => {
- // TODO handle other statuses
- if (this.mounted) {
- if ([403, 404].includes(response.status)) {
- reject(response);
- } else {
- resolve([]);
- }
- }
- };
-
- const from = this.props.aroundLine
- ? Math.max(1, this.props.aroundLine - LINES_TO_LOAD / 2 + 1)
- : 1;
-
- 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++;
-
- this.loadSources(this.props.component, from, to, this.props.branchLike).then(sources => {
- resolve(sources);
- }, onFailLoadSources);
- });
- };
-
- loadSourcesBefore = () => {
- 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);
- Promise.all([
- this.loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike),
- this.loadIssues(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike)
- ]).then(
- ([sources, issues]) => {
- if (this.mounted) {
- this.setState(prevState => {
- const nextIssues = uniqBy([...issues, ...(prevState.issues || [])], issue => issue.key);
- return {
- issues: nextIssues,
- issuesByLine: issuesByLine(nextIssues),
- issueLocationsByLine: locationsByLine(nextIssues),
- loadingSourcesBefore: false,
- sources: [...this.computeCoverageStatus(sources), ...(prevState.sources || [])],
- symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) }
- };
- });
- }
- },
- () => {
- // TODO
- }
- );
- };
-
- loadSourcesAfter = () => {
- if (!this.state.sources) {
- return;
- }
- const lastSourceLine = this.state.sources[this.state.sources.length - 1];
- this.setState({ loadingSourcesAfter: true });
- const fromLine = lastSourceLine.line + 1;
- // request one additional line to define `hasSourcesAfter`
- const toLine = lastSourceLine.line + LINES_TO_LOAD + 1;
- Promise.all([
- this.loadSources(this.props.component, fromLine, toLine, this.props.branchLike),
- this.loadIssues(this.props.component, fromLine, toLine, this.props.branchLike)
- ]).then(
- ([sources, issues]) => {
- if (this.mounted) {
- this.setState(prevState => {
- const nextIssues = uniqBy([...(prevState.issues || []), ...issues], issue => issue.key);
- return {
- issues: nextIssues,
- issuesByLine: issuesByLine(nextIssues),
- issueLocationsByLine: locationsByLine(nextIssues),
- hasSourcesAfter: sources.length > LINES_TO_LOAD,
- loadingSourcesAfter: false,
- sources: [
- ...(prevState.sources || []),
- ...this.computeCoverageStatus(sources.slice(0, LINES_TO_LOAD))
- ],
- symbolsByLine: {
- ...prevState.symbolsByLine,
- ...symbolsByLine(sources.slice(0, LINES_TO_LOAD))
- }
- };
- });
- }
- },
- () => {
- // TODO
- }
- );
- };
-
- loadDuplications = () => {
- getDuplications({
- key: this.props.component,
- ...getBranchLikeQuery(this.props.branchLike)
- }).then(
- r => {
- if (this.mounted) {
- this.setState({
- duplications: r.duplications,
- duplicationsByLine: duplicationsByLine(r.duplications),
- duplicatedFiles: r.files
- });
- }
- },
- () => {
- // TODO
- }
- );
- };
-
- handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
- 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;
- });
- };
-
- handleSymbolClick = (symbols: string[]) => {
- this.setState(state => {
- const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0;
- const highlightedSymbols = shouldDisable ? [] : symbols;
- return { highlightedSymbols };
- });
- };
-
- handleIssueSelect = (issue: string) => {
- if (this.props.onIssueSelect) {
- this.props.onIssueSelect(issue);
- } else {
- this.setState({ selectedIssue: issue });
- }
- };
-
- handleIssueUnselect = () => {
- if (this.props.onIssueUnselect) {
- this.props.onIssueUnselect();
- } else {
- this.setState({ selectedIssue: undefined });
- }
- };
-
- handleOpenIssues = (line: SourceLine) => {
- this.setState(state => ({
- openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
- }));
- };
-
- handleCloseIssues = (line: SourceLine) => {
- this.setState(state => ({
- openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
- }));
- };
-
- handleIssueChange = (issue: Issue) => {
- this.setState(({ issues = [] }) => {
- const newIssues = issues.map(candidate => (candidate.key === issue.key ? issue : candidate));
- return { issues: newIssues, issuesByLine: issuesByLine(newIssues) };
- });
- if (this.props.onIssueChange) {
- this.props.onIssueChange(issue);
- }
- };
-
- renderDuplicationPopup = (index: number, line: number) => {
- const { component, 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)}
- openComponent={openComponent}
- sourceViewerFile={component}
- />
- )}
- </WorkspaceContext.Consumer>
- );
- };
-
- renderCode(sources: SourceLine[]) {
- const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
- return (
- <SourceViewerCode
- branchLike={this.props.branchLike}
- displayAllIssues={this.props.displayAllIssues}
- displayIssueLocationsCount={this.props.displayIssueLocationsCount}
- displayIssueLocationsLink={this.props.displayIssueLocationsLink}
- displayLocationMarkers={this.props.displayLocationMarkers}
- duplications={this.state.duplications}
- duplicationsByLine={this.state.duplicationsByLine}
- hasSourcesAfter={this.state.hasSourcesAfter}
- hasSourcesBefore={hasSourcesBefore}
- highlightedLine={this.props.highlightedLine}
- highlightedLocationMessage={this.props.highlightedLocationMessage}
- highlightedLocations={this.props.highlightedLocations}
- highlightedSymbols={this.state.highlightedSymbols}
- issueLocationsByLine={this.state.issueLocationsByLine}
- issuePopup={this.state.issuePopup}
- 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}
- onIssueChange={this.handleIssueChange}
- onIssuePopupToggle={this.handleIssuePopupToggle}
- onIssueSelect={this.handleIssueSelect}
- onIssueUnselect={this.handleIssueUnselect}
- onIssuesClose={this.handleCloseIssues}
- onIssuesOpen={this.handleOpenIssues}
- onLocationSelect={this.props.onLocationSelect}
- onSymbolClick={this.handleSymbolClick}
- openIssuesByLine={this.state.openIssuesByLine}
- renderDuplicationPopup={this.renderDuplicationPopup}
- scroll={this.props.scroll}
- metricKey={this.props.metricKey}
- selectedIssue={this.state.selectedIssue}
- sources={sources}
- symbolsByLine={this.state.symbolsByLine}
- />
- );
- }
-
- renderHeader(branchLike: BranchLike | undefined, sourceViewerFile: SourceViewerFile) {
- return this.props.slimHeader ? (
- <SourceViewerHeaderSlim branchLike={branchLike} sourceViewerFile={sourceViewerFile} />
- ) : (
- <WorkspaceContext.Consumer>
- {({ openComponent }) => (
- <SourceViewerHeader
- branchLike={this.props.branchLike}
- componentMeasures={this.props.componentMeasures}
- openComponent={openComponent}
- showMeasures={this.props.showMeasures}
- sourceViewerFile={sourceViewerFile}
- />
- )}
- </WorkspaceContext.Consumer>
- );
- }
-
- render() {
- const { component, loading, sources, notAccessible, sourceRemoved } = this.state;
-
- if (loading) {
- return null;
- }
-
- if (this.state.notExist) {
- return (
- <Alert className="spacer-top" variant="warning">
- {translate('component_viewer.no_component')}
- </Alert>
- );
- }
-
- if (notAccessible) {
- return (
- <Alert className="spacer-top" variant="warning">
- {translate('code_viewer.no_source_code_displayed_due_to_security')}
- </Alert>
- );
- }
-
- if (!component) {
- return null;
- }
-
- return (
- <SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
- <div className="source-viewer" ref={node => (this.node = node)}>
- {this.renderHeader(this.props.branchLike, 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>
- );
- }
-}
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 c51ecb927c6..e8a35ab3ad9 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
@@ -21,12 +21,13 @@ import { queryHelpers, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { SourceViewerServiceMock } from '../../../api/mocks/SourceViewerServiceMock';
+import { HttpStatus } from '../../../helpers/request';
import { mockIssue } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
import SourceViewer from '../SourceViewer';
-import SourceViewerBase from '../SourceViewerBase';
jest.mock('../../../api/components');
+jest.mock('../../../api/issues');
jest.mock('../helpers/lines', () => {
const lines = jest.requireActual('../helpers/lines');
return {
@@ -37,6 +38,10 @@ jest.mock('../helpers/lines', () => {
const handler = new SourceViewerServiceMock();
+beforeEach(() => {
+ handler.reset();
+});
+
it('should show a permalink on line number', async () => {
const user = userEvent.setup();
renderSourceViewer();
@@ -108,6 +113,53 @@ it('should show issue on empty file', async () => {
expect(await screen.findByRole('row', { name: 'First Issue' })).toBeInTheDocument();
});
+it('should be able to interact with issue action', async () => {
+ const user = userEvent.setup();
+ renderSourceViewer({
+ loadIssues: jest.fn().mockResolvedValue([
+ mockIssue(false, {
+ actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'],
+ key: 'first-issue',
+ message: 'First Issue',
+ line: 1,
+ textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }
+ })
+ ])
+ });
+
+ //Open Issue type
+ await user.click(
+ await screen.findByRole('button', { name: 'issue.type.type_x_click_to_change.issue.type.BUG' })
+ );
+ expect(screen.getByRole('link', { name: 'issue.type.CODE_SMELL' })).toBeInTheDocument();
+
+ // Open severity
+ await user.click(
+ await screen.findByRole('button', {
+ name: 'issue.severity.severity_x_click_to_change.severity.MAJOR'
+ })
+ );
+ expect(screen.getByRole('link', { name: 'severity.MINOR' })).toBeInTheDocument();
+
+ // Close
+ await user.keyboard('{Escape}');
+ expect(screen.queryByRole('link', { name: 'severity.MINOR' })).not.toBeInTheDocument();
+
+ // Change the severity
+ await user.click(
+ await screen.findByRole('button', {
+ name: 'issue.severity.severity_x_click_to_change.severity.MAJOR'
+ })
+ );
+ expect(screen.getByRole('link', { name: 'severity.MINOR' })).toBeInTheDocument();
+ await user.click(screen.getByRole('link', { name: 'severity.MINOR' }));
+ expect(
+ screen.getByRole('button', {
+ name: 'issue.severity.severity_x_click_to_change.severity.MINOR'
+ })
+ ).toBeInTheDocument();
+});
+
it('should load line when looking arround unloaded line', async () => {
const { rerender } = renderSourceViewer({
aroundLine: 50,
@@ -299,11 +351,37 @@ it('should show duplication block', async () => {
expect(duplicateLine.queryByRole('link', { name: 'test2.js' })).not.toBeInTheDocument();
});
-function renderSourceViewer(override?: Partial<SourceViewerBase['props']>) {
+it('should highlight symbol', async () => {
+ const user = userEvent.setup();
+ renderSourceViewer({ component: 'project:testSymb.tsx' });
+ const symbols = await screen.findAllByText('symbole');
+ await user.click(symbols[0]);
+
+ // For now just check the class. Maybe found a better accessible way of showing higlighted symbole
+ symbols.forEach(element => {
+ expect(element).toHaveClass('highlighted');
+ });
+});
+
+it('should show correct message when component is not asscessible', async () => {
+ handler.setFailLoadingComponentStatus(HttpStatus.Forbidden);
+ renderSourceViewer();
+ expect(
+ await screen.findByText('code_viewer.no_source_code_displayed_due_to_security')
+ ).toBeInTheDocument();
+});
+
+it('should show correct message when component does not exist', async () => {
+ handler.setFailLoadingComponentStatus(HttpStatus.NotFound);
+ renderSourceViewer();
+ expect(await screen.findByText('component_viewer.no_component')).toBeInTheDocument();
+});
+
+function renderSourceViewer(override?: Partial<SourceViewer['props']>) {
return renderComponent(getSourceViewerUi(override));
}
-function getSourceViewerUi(override?: Partial<SourceViewerBase['props']>) {
+function getSourceViewerUi(override?: Partial<SourceViewer['props']>) {
return (
<SourceViewer
aroundLine={1}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerBase-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx
index 66cb69fdb7e..ac622f4052e 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerBase-test.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx
@@ -25,7 +25,7 @@ import { mockSourceLine, mockSourceViewerFile } from '../../../helpers/mocks/sou
import { mockIssue } from '../../../helpers/testMocks';
import { waitAndUpdate } from '../../../helpers/testUtils';
import defaultLoadIssues from '../helpers/loadIssues';
-import SourceViewerBase from '../SourceViewerBase';
+import SourceViewer from '../SourceViewer';
jest.mock('../helpers/loadIssues', () => jest.fn().mockRejectedValue({}));
@@ -148,8 +148,8 @@ it('should handle no sources when checking ranges', () => {
expect(wrapper.instance().isLineOutsideOfRange(12)).toBe(true);
});
-function shallowRender(overrides: Partial<SourceViewerBase['props']> = {}) {
- return shallow<SourceViewerBase>(
- <SourceViewerBase branchLike={mockMainBranch()} component="my-component" {...overrides} />
+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__/SourceViewerBase-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap
index ef8cdb4d1ba..ef8cdb4d1ba 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerBase-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/components/__tests__/__snapshots__/lazyLoadComponent-test.tsx.snap b/server/sonar-web/src/main/js/components/__tests__/__snapshots__/lazyLoadComponent-test.tsx.snap
deleted file mode 100644
index 9edbac96c7c..00000000000
--- a/server/sonar-web/src/main/js/components/__tests__/__snapshots__/lazyLoadComponent-test.tsx.snap
+++ /dev/null
@@ -1,36 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should correctly set given display name 1`] = `
-<div>
- <CustomDisplayName />
-</div>
-`;
-
-exports[`should lazy load and display the component 1`] = `
-<LazyComponentWrapper>
- <LazyErrorBoundary>
- <Suspense
- fallback={null}
- />
- </LazyErrorBoundary>
-</LazyComponentWrapper>
-`;
-
-exports[`should lazy load and display the component 2`] = `
-<LazyComponentWrapper>
- <LazyErrorBoundary>
- <Suspense
- fallback={null}
- >
- <Checkbox>
- <a
- className="icon-checkbox"
- href="#"
- onClick={[Function]}
- role="checkbox"
- />
- </Checkbox>
- </Suspense>
- </LazyErrorBoundary>
-</LazyComponentWrapper>
-`;
diff --git a/server/sonar-web/src/main/js/components/__tests__/lazyLoadComponent-test.tsx b/server/sonar-web/src/main/js/components/__tests__/lazyLoadComponent-test.tsx
deleted file mode 100644
index 357fb135ddc..00000000000
--- a/server/sonar-web/src/main/js/components/__tests__/lazyLoadComponent-test.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { mount, shallow } from 'enzyme';
-import * as React from 'react';
-import { waitAndUpdate } from '../../helpers/testUtils';
-import { lazyLoadComponent } from '../lazyLoadComponent';
-
-const factory = jest.fn().mockImplementation(() => import('../../components/controls/Checkbox'));
-
-beforeEach(() => {
- factory.mockClear();
- jest.useFakeTimers();
-});
-
-afterAll(() => {
- jest.runOnlyPendingTimers();
- jest.useRealTimers();
-});
-
-it('should lazy load and display the component', async () => {
- const LazyComponent = lazyLoadComponent(factory);
- const wrapper = mount(<LazyComponent />);
- expect(wrapper).toMatchSnapshot();
- expect(factory).toBeCalledTimes(1);
- jest.runOnlyPendingTimers();
- await waitAndUpdate(wrapper);
- jest.runOnlyPendingTimers();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
- expect(factory).toBeCalledTimes(1);
-});
-
-it('should correctly handle import errors', () => {
- const LazyComponent = lazyLoadComponent(factory);
- const wrapper = mount(<LazyComponent />);
- wrapper.find('Suspense').simulateError({ request: 'test' });
- expect(wrapper.find('Alert').exists()).toBe(true);
-});
-
-it('should correctly set given display name', () => {
- const LazyComponent = lazyLoadComponent(factory, 'CustomDisplayName');
- const wrapper = shallow(
- <div>
- <LazyComponent />
- </div>
- );
- expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.tsx b/server/sonar-web/src/main/js/components/controls/DateInput.tsx
index 4658ac4eb58..519b7850b2c 100644
--- a/server/sonar-web/src/main/js/components/controls/DateInput.tsx
+++ b/server/sonar-web/src/main/js/components/controls/DateInput.tsx
@@ -21,7 +21,7 @@ import classNames from 'classnames';
import { addMonths, setMonth, setYear, subMonths } from 'date-fns';
import { range } from 'lodash';
import * as React from 'react';
-import { DayModifiers, Modifier, Modifiers } from 'react-day-picker';
+import DayPicker, { DayModifiers, Modifier, Modifiers } from 'react-day-picker';
import { injectIntl, WrappedComponentProps } from 'react-intl';
import { ButtonIcon, ClearButton } from '../../components/controls/buttons';
import OutsideClickHandler from '../../components/controls/OutsideClickHandler';
@@ -29,13 +29,10 @@ import CalendarIcon from '../../components/icons/CalendarIcon';
import ChevronLeftIcon from '../../components/icons/ChevronLeftIcon';
import ChevronRightIcon from '../../components/icons/ChevronRightIcon';
import { getShortMonthName, getShortWeekDayName, getWeekDayName } from '../../helpers/l10n';
-import { lazyLoadComponent } from '../lazyLoadComponent';
import './DayPicker.css';
import Select from './Select';
import './styles.css';
-const DayPicker = lazyLoadComponent(() => import('react-day-picker'), 'DayPicker');
-
interface Props {
className?: string;
currentMonth?: Date;
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap
index 06c3d24e547..3e58abef40e 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap
@@ -229,23 +229,88 @@ exports[`should render 3`] = `
</ButtonIcon>
</nav>
<DayPicker
+ canChangeMonth={true}
captionElement={<NullComponent />}
+ classNames={
+ Object {
+ "body": "DayPicker-Body",
+ "caption": "DayPicker-Caption",
+ "container": "DayPicker",
+ "day": "DayPicker-Day",
+ "disabled": "disabled",
+ "footer": "DayPicker-Footer",
+ "interactionDisabled": "DayPicker--interactionDisabled",
+ "month": "DayPicker-Month",
+ "months": "DayPicker-Months",
+ "navBar": "DayPicker-NavBar",
+ "navButtonInteractionDisabled": "DayPicker-NavButton--interactionDisabled",
+ "navButtonNext": "DayPicker-NavButton DayPicker-NavButton--next",
+ "navButtonPrev": "DayPicker-NavButton DayPicker-NavButton--prev",
+ "outside": "outside",
+ "selected": "selected",
+ "today": "today",
+ "todayButton": "DayPicker-TodayButton",
+ "week": "DayPicker-Week",
+ "weekNumber": "DayPicker-WeekNumber",
+ "weekday": "DayPicker-Weekday",
+ "weekdays": "DayPicker-Weekdays",
+ "weekdaysRow": "DayPicker-WeekdaysRow",
+ "wrapper": "DayPicker-wrapper",
+ }
+ }
disabledDays={
Object {
"after": 2018-02-05T00:00:00.000Z,
"before": 2018-01-17T00:00:00.000Z,
}
}
+ enableOutsideDaysClick={true}
firstDayOfWeek={1}
+ fixedWeeks={false}
+ labels={
+ Object {
+ "nextMonth": "Next Month",
+ "previousMonth": "Previous Month",
+ }
+ }
+ locale="en"
+ localeUtils={
+ Object {
+ "default": Object {
+ "formatDay": [Function],
+ "formatMonthTitle": [Function],
+ "formatWeekdayLong": [Function],
+ "formatWeekdayShort": [Function],
+ "getFirstDayOfWeek": [Function],
+ "getMonths": [Function],
+ },
+ "formatDay": [Function],
+ "formatMonthTitle": [Function],
+ "formatWeekdayLong": [Function],
+ "formatWeekdayShort": [Function],
+ "getFirstDayOfWeek": [Function],
+ "getMonths": [Function],
+ }
+ }
month={2018-01-17T00:00:00.000Z}
navbarElement={<NullComponent />}
+ numberOfMonths={1}
onDayClick={[Function]}
onDayMouseEnter={[Function]}
+ pagedNavigation={false}
+ renderDay={[Function]}
+ renderWeek={[Function]}
+ reverseMonths={false}
selectedDays={
Array [
2018-01-17T00:00:00.000Z,
]
}
+ showOutsideDays={false}
+ showWeekDays={true}
+ showWeekNumbers={false}
+ tabIndex={0}
+ weekdayElement={<Weekday />}
weekdaysLong={
Array [
"Sunday",
diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
index f34140b5172..00ba400af41 100644
--- a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
+++ b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
@@ -22,9 +22,7 @@ import { translate } from '../../helpers/l10n';
import { ButtonLink } from '../controls/buttons';
import Toggler from '../controls/Toggler';
import HelpIcon from '../icons/HelpIcon';
-import { lazyLoadComponent } from '../lazyLoadComponent';
-
-const EmbedDocsPopup = lazyLoadComponent(() => import('./EmbedDocsPopup'));
+import EmbedDocsPopup from './EmbedDocsPopup';
interface State {
helpOpen: boolean;
diff --git a/server/sonar-web/src/main/js/components/lazyLoadComponent.tsx b/server/sonar-web/src/main/js/components/lazyLoadComponent.tsx
deleted file mode 100644
index 288433be1e8..00000000000
--- a/server/sonar-web/src/main/js/components/lazyLoadComponent.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 * as React from 'react';
-import { Alert } from '../components/ui/Alert';
-import { IS_SSR } from '../helpers/browser';
-import { translate } from '../helpers/l10n';
-import { requestTryAndRepeatUntil } from '../helpers/request';
-
-export function lazyLoadComponent<T extends React.ComponentType<any>>(
- factory: () => Promise<{ default: T }>,
- displayName?: string
-) {
- const LazyComponent = React.lazy(() =>
- requestTryAndRepeatUntil(factory, { max: 2, slowThreshold: 2 }, () => true)
- );
-
- function LazyComponentWrapper(props: React.ComponentProps<T>) {
- if (IS_SSR) {
- return null;
- }
- return (
- <LazyErrorBoundary>
- <React.Suspense fallback={null}>
- <LazyComponent {...props} />
- </React.Suspense>
- </LazyErrorBoundary>
- );
- }
-
- LazyComponentWrapper.displayName = displayName;
- return LazyComponentWrapper;
-}
-
-interface ErrorBoundaryProps {
- children: React.ReactNode;
-}
-
-interface ErrorBoundaryState {
- hasError: boolean;
-}
-
-export class LazyErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
- state: ErrorBoundaryState = { hasError: false };
-
- static getDerivedStateFromError() {
- // Update state so the next render will show the fallback UI.
- return { hasError: true };
- }
-
- render() {
- if (this.state.hasError) {
- return <Alert variant="error">{translate('default_error_message')}</Alert>;
- }
- return this.props.children;
- }
-}
diff --git a/server/sonar-web/src/main/js/components/ui/CoverageRating.tsx b/server/sonar-web/src/main/js/components/ui/CoverageRating.tsx
index f90c26380de..3c4f2d3de9e 100644
--- a/server/sonar-web/src/main/js/components/ui/CoverageRating.tsx
+++ b/server/sonar-web/src/main/js/components/ui/CoverageRating.tsx
@@ -19,12 +19,7 @@
*/
import * as React from 'react';
import { colors } from '../../app/theme';
-import { lazyLoadComponent } from '../lazyLoadComponent';
-
-const DonutChart = lazyLoadComponent(
- () => import('../../components/charts/DonutChart'),
- 'DonutChart'
-);
+import DonutChart from '../../components/charts/DonutChart';
const SIZE_TO_WIDTH_MAPPING = { small: 16, normal: 24, big: 40, huge: 60 };
diff --git a/server/sonar-web/src/main/js/components/workspace/Workspace.tsx b/server/sonar-web/src/main/js/components/workspace/Workspace.tsx
index 63fa2998727..d8436587217 100644
--- a/server/sonar-web/src/main/js/components/workspace/Workspace.tsx
+++ b/server/sonar-web/src/main/js/components/workspace/Workspace.tsx
@@ -22,22 +22,14 @@ import * as React from 'react';
import { getRulesApp } from '../../api/rules';
import { get, save } from '../../helpers/storage';
import { Dict } from '../../types/types';
-import { lazyLoadComponent } from '../lazyLoadComponent';
import { ComponentDescriptor, RuleDescriptor, WorkspaceContext } from './context';
import './styles.css';
+import WorkspaceComponentViewer from './WorkspaceComponentViewer';
+import WorkspaceNav from './WorkspaceNav';
import WorkspacePortal from './WorkspacePortal';
+import WorkspaceRuleViewer from './WorkspaceRuleViewer';
const WORKSPACE = 'sonarqube-workspace';
-const WorkspaceNav = lazyLoadComponent(() => import('./WorkspaceNav'), 'WorkspaceNav');
-const WorkspaceRuleViewer = lazyLoadComponent(
- () => import('./WorkspaceRuleViewer'),
- 'WorkspaceRuleViewer'
-);
-const WorkspaceComponentViewer = lazyLoadComponent(
- () => import('./WorkspaceComponentViewer'),
- 'WorkspaceComponentViewer'
-);
-
interface State {
components: ComponentDescriptor[];
externalRulesRepoNames: Dict<string>;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap
index b94880cf499..95482d97039 100644
--- a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap
@@ -58,7 +58,7 @@ exports[`should render correctly: open component 1`] = `
}
rules={Array []}
/>
- <WorkspaceComponentViewer
+ <withBranchStatusActions(WorkspaceComponentViewer)
component={
Object {
"branchLike": Object {
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
index 72da3f75715..477fa047ae1 100644
--- 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
@@ -30,6 +30,10 @@ exports[`should render 1`] = `
>
<SourceViewer
component="foo"
+ displayAllIssues={false}
+ displayIssueLocationsCount={true}
+ displayIssueLocationsLink={true}
+ displayLocationMarkers={true}
onIssueChange={[Function]}
onLoaded={[Function]}
/>
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
index a46d5ae3f83..807dda6edad 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
@@ -474,24 +474,9 @@ describe('#getHostUrl', () => {
jest.mock('../system', () => ({
getBaseUrl: () => ''
}));
- jest.mock('../browser', () => ({
- IS_SSR: false
- }));
const mockedUrls = require('../urls');
expect(mockedUrls.getHostUrl()).toBe('http://localhost');
});
- it('should throw on server-side', () => {
- jest.mock('../system', () => ({
- getBaseUrl: () => ''
- }));
- jest.mock('../browser', () => ({
- IS_SSR: true
- }));
- const mockedUrls = require('../urls');
- expect(mockedUrls.getHostUrl).toThrowErrorMatchingInlineSnapshot(
- `"No host url available on server side."`
- );
- });
});
describe('searchParamsToQuery', () => {
diff --git a/server/sonar-web/src/main/js/helpers/browser.ts b/server/sonar-web/src/main/js/helpers/browser.ts
index 7f73f29b63b..1b6991bfbe9 100644
--- a/server/sonar-web/src/main/js/helpers/browser.ts
+++ b/server/sonar-web/src/main/js/helpers/browser.ts
@@ -19,8 +19,6 @@
*/
import { EnhancedWindow } from '../types/browser';
-export const IS_SSR = typeof window === 'undefined';
-
export function getEnhancedWindow() {
return (window as unknown) as EnhancedWindow;
}
diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
index 840ade86c20..597d2a79802 100644
--- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
+++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
@@ -25,6 +25,7 @@ import { IntlProvider } from 'react-intl';
import { MemoryRouter, Outlet, parsePath, Route, Routes } from 'react-router-dom';
import AdminContext from '../app/components/AdminContext';
import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider';
+import { ComponentContext } from '../app/components/componentContext/ComponentContext';
import CurrentUserContextProvider from '../app/components/current-user/CurrentUserContextProvider';
import GlobalMessagesContainer from '../app/components/GlobalMessagesContainer';
import IndexationContextProvider from '../app/components/indexation/IndexationContextProvider';
@@ -32,6 +33,7 @@ import { LanguagesContext } from '../app/components/languages/LanguagesContext';
import { MetricsContext } from '../app/components/metrics/MetricsContext';
import { useLocation } from '../components/hoc/withRouter';
import { AppState } from '../types/appstate';
+import { ComponentContextShape } from '../types/component';
import { Dict, Extension, Languages, Metric, SysStatus } from '../types/types';
import { CurrentUser } from '../types/users';
import { DEFAULT_METRICS } from './mocks/metrics';
@@ -45,7 +47,7 @@ interface RenderContext {
navigateTo?: string;
}
-export function renderAdminApp(
+export function renderAppWithAdminContext(
indexPath: string,
routes: () => JSX.Element,
context: RenderContext = {},
@@ -98,7 +100,34 @@ export function renderComponent(component: React.ReactElement, pathname = '/') {
return render(component, { wrapper: Wrapper });
}
-export function renderComponentApp(
+export function renderAppWithComponentContext(
+ indexPath: string,
+ routes: () => JSX.Element,
+ context: RenderContext = {},
+ componentContext?: Partial<ComponentContextShape>
+) {
+ function MockComponentContainer() {
+ return (
+ <ComponentContext.Provider
+ value={{
+ branchLikes: [],
+ onBranchesChange: jest.fn(),
+ onComponentChange: jest.fn(),
+ ...componentContext
+ }}>
+ <Outlet />
+ </ComponentContext.Provider>
+ );
+ }
+
+ return renderRoutedApp(
+ <Route element={<MockComponentContainer />}>{routes()}</Route>,
+ indexPath,
+ context
+ );
+}
+
+export function renderApp(
indexPath: string,
component: JSX.Element,
context: RenderContext = {}
@@ -106,7 +135,7 @@ export function renderComponentApp(
return renderRoutedApp(<Route path={indexPath} element={component} />, indexPath, context);
}
-export function renderApp(
+export function renderAppRoutes(
indexPath: string,
routes: () => JSX.Element,
context?: RenderContext
diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts
index 0e7ab419f9c..1cc4e644fb0 100644
--- a/server/sonar-web/src/main/js/helpers/urls.ts
+++ b/server/sonar-web/src/main/js/helpers/urls.ts
@@ -28,7 +28,6 @@ import { SecurityStandard } from '../types/security';
import { Dict, RawQuery } from '../types/types';
import { HomePage } from '../types/users';
import { getBranchLikeQuery, isBranch, isMainBranch, isPullRequest } from './branch-like';
-import { IS_SSR } from './browser';
import { serializeOptionalBoolean } from './query';
import { getBaseUrl } from './system';
@@ -408,9 +407,6 @@ export function stripTrailingSlash(url: string) {
}
export function getHostUrl(): string {
- if (IS_SSR) {
- throw new Error('No host url available on server side.');
- }
return window.location.origin + getBaseUrl();
}