@@ -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)); | |||
} | |||
} |
@@ -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)); | |||
} |
@@ -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; |
@@ -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 ( |
@@ -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; | |||
} |
@@ -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!'); |
@@ -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> |
@@ -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 | |||
}); | |||
} |
@@ -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; |
@@ -31,7 +31,7 @@ exports[`should render 1`] = ` | |||
} | |||
/> | |||
</Alert> | |||
<AnalysisWarningsModal | |||
<withCurrentUserContext(AnalysisWarningsModal) | |||
componentKey="foo" | |||
onClose={[Function]} | |||
onWarningDismiss={[MockFunction]} |
@@ -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; |
@@ -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 }); | |||
} |
@@ -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 }); | |||
} |
@@ -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, {}); | |||
} |
@@ -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>; |
@@ -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']>) { |
@@ -158,7 +158,7 @@ exports[`shows stack trace 1`] = ` | |||
`; | |||
exports[`shows warnings 1`] = ` | |||
<AnalysisWarningsModal | |||
<withCurrentUserContext(AnalysisWarningsModal) | |||
componentKey="foo" | |||
onClose={[Function]} | |||
taskId="AXR8jg_0mF2ZsYr8Wzs2" |
@@ -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() }); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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: { |
@@ -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 }); | |||
} |
@@ -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]} | |||
/> |
@@ -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> |
@@ -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 }); | |||
} |
@@ -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 | |||
); |
@@ -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 | |||
); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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' })} |
@@ -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 />; | |||
} |
@@ -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; |
@@ -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; |
@@ -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={{ |
@@ -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 /> |
@@ -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')); |
@@ -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); |
@@ -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} />; | |||
} |
@@ -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>; | |||
} | |||
); |
@@ -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, {}); | |||
} |
@@ -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 }); | |||
} |
@@ -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>; |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} |
@@ -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} /> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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(); | |||
}); |
@@ -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; |
@@ -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", |
@@ -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; |
@@ -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; | |||
} | |||
} |
@@ -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 }; | |||
@@ -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>; |
@@ -58,7 +58,7 @@ exports[`should render correctly: open component 1`] = ` | |||
} | |||
rules={Array []} | |||
/> | |||
<WorkspaceComponentViewer | |||
<withBranchStatusActions(WorkspaceComponentViewer) | |||
component={ | |||
Object { | |||
"branchLike": Object { |
@@ -30,6 +30,10 @@ exports[`should render 1`] = ` | |||
> | |||
<SourceViewer | |||
component="foo" | |||
displayAllIssues={false} | |||
displayIssueLocationsCount={true} | |||
displayIssueLocationsLink={true} | |||
displayLocationMarkers={true} | |||
onIssueChange={[Function]} | |||
onLoaded={[Function]} | |||
/> |
@@ -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', () => { |
@@ -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; | |||
} |
@@ -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 |
@@ -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(); | |||
} | |||