diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2022-06-21 09:24:30 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-07-20 20:03:25 +0000 |
commit | 0a7d6f7245e753feb25bceae733fd659591b8f81 (patch) | |
tree | 1f40d8439a8d7543dd0828a884868aada5a1810f | |
parent | 466c012d87ca3f37ead691160afb87b212d01645 (diff) | |
download | sonarqube-0a7d6f7245e753feb25bceae733fd659591b8f81.tar.gz sonarqube-0a7d6f7245e753feb25bceae733fd659591b8f81.zip |
SONAR-16475 Remove lazyLoading of frontend component
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(); } |