/* | |||||
* 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)); | |||||
} | |||||
} |
getSources | getSources | ||||
} from '../../api/components'; | } from '../../api/components'; | ||||
import { mockSourceLine } from '../../helpers/mocks/sources'; | import { mockSourceLine } from '../../helpers/mocks/sources'; | ||||
import { HttpStatus } from '../../helpers/request'; | |||||
import { BranchParameters } from '../../types/branch-like'; | import { BranchParameters } from '../../types/branch-like'; | ||||
import { Dict } from '../../types/types'; | import { Dict } from '../../types/types'; | ||||
import { setIssueSeverity } from '../issues'; | |||||
function mockSourceFileView(name: string) { | |||||
function mockSourceFileView(name: string, project = 'project') { | |||||
return { | return { | ||||
component: { | component: { | ||||
key: `project:${name}`, | |||||
key: `${project}:${name}`, | |||||
name, | name, | ||||
qualifier: 'FIL', | qualifier: 'FIL', | ||||
path: name, | path: name, | ||||
needIssueSync: false | needIssueSync: false | ||||
}, | }, | ||||
sourceFileView: { | sourceFileView: { | ||||
key: `project:${name}`, | |||||
uuid: 'AWMgNpveti8CNlpVyHAm', | |||||
key: `${project}:${name}`, | |||||
uuid: `AWMgNpveti8CNlpVyHAm${project}${name}`, | |||||
path: name, | path: name, | ||||
name, | name, | ||||
longName: name, | longName: name, | ||||
q: 'FIL', | q: 'FIL', | ||||
project: 'project', | |||||
project, | |||||
projectName: 'Test project', | projectName: 'Test project', | ||||
fav: false, | fav: false, | ||||
canMarkAsFavorite: true, | canMarkAsFavorite: true, | ||||
), | ), | ||||
ancestors: ANCESTORS | 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': { | 'project:test.js': { | ||||
...mockSourceFileView('test.js'), | ...mockSourceFileView('test.js'), | ||||
sources: [ | sources: [ | ||||
}; | }; | ||||
export class SourceViewerServiceMock { | export class SourceViewerServiceMock { | ||||
faileLoadingComponentStatus: HttpStatus | undefined = undefined; | |||||
constructor() { | constructor() { | ||||
(getComponentData as jest.Mock).mockImplementation(this.handleGetComponentData); | (getComponentData as jest.Mock).mockImplementation(this.handleGetComponentData); | ||||
(getComponentForSourceViewer as jest.Mock).mockImplementation( | (getComponentForSourceViewer as jest.Mock).mockImplementation( | ||||
); | ); | ||||
(getDuplications as jest.Mock).mockImplementation(this.handleGetDuplications); | (getDuplications as jest.Mock).mockImplementation(this.handleGetDuplications); | ||||
(getSources as jest.Mock).mockImplementation(this.handleGetSources); | (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 { | getHugeFile(): string { | ||||
}; | }; | ||||
handleGetComponentData = (data: { component: string } & BranchParameters) => { | handleGetComponentData = (data: { component: string } & BranchParameters) => { | ||||
if (this.faileLoadingComponentStatus !== undefined) { | |||||
return Promise.reject({ status: this.faileLoadingComponentStatus }); | |||||
} | |||||
return this.reply(pick(FILES[data.component], ['component', 'ancestor'])); | return this.reply(pick(FILES[data.component], ['component', 'ancestor'])); | ||||
}; | }; | ||||
handleSetIssueSeverity = () => { | |||||
return this.reply({}); | |||||
}; | |||||
reply<T>(response: T): Promise<T> { | reply<T>(response: T): Promise<T> { | ||||
return Promise.resolve(cloneDeep(response)); | return Promise.resolve(cloneDeep(response)); | ||||
} | } |
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { Outlet } from 'react-router-dom'; | import { Outlet } from 'react-router-dom'; | ||||
import { lazyLoadComponent } from '../../components/lazyLoadComponent'; | |||||
import { AppState } from '../../types/appstate'; | import { AppState } from '../../types/appstate'; | ||||
import { GlobalSettingKeys } from '../../types/settings'; | import { GlobalSettingKeys } from '../../types/settings'; | ||||
import withAppStateContext from './app-state/withAppStateContext'; | import withAppStateContext from './app-state/withAppStateContext'; | ||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal'; | import KeyboardShortcutsModal from './KeyboardShortcutsModal'; | ||||
const PageTracker = lazyLoadComponent(() => import('./PageTracker')); | |||||
import PageTracker from './PageTracker'; | |||||
interface Props { | interface Props { | ||||
appState: AppState; | appState: AppState; |
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { Outlet } from 'react-router-dom'; | import { Outlet } from 'react-router-dom'; | ||||
import { lazyLoadComponent } from '../../components/lazyLoadComponent'; | |||||
import GlobalFooter from './GlobalFooter'; | import GlobalFooter from './GlobalFooter'; | ||||
const PageTracker = lazyLoadComponent(() => import('./PageTracker')); | |||||
import PageTracker from './PageTracker'; | |||||
export default function SimpleSessionsContainer() { | export default function SimpleSessionsContainer() { | ||||
return ( | return ( |
import { differenceInDays } from 'date-fns'; | import { differenceInDays } from 'date-fns'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { showLicense } from '../../api/editions'; | import { showLicense } from '../../api/editions'; | ||||
import LicensePromptModal from '../../apps/marketplace/components/LicensePromptModal'; | |||||
import { Location, Router, withRouter } from '../../components/hoc/withRouter'; | import { Location, Router, withRouter } from '../../components/hoc/withRouter'; | ||||
import { lazyLoadComponent } from '../../components/lazyLoadComponent'; | |||||
import { parseDate, toShortNotSoISOString } from '../../helpers/dates'; | import { parseDate, toShortNotSoISOString } from '../../helpers/dates'; | ||||
import { hasMessage } from '../../helpers/l10n'; | import { hasMessage } from '../../helpers/l10n'; | ||||
import { get, save } from '../../helpers/storage'; | import { get, save } from '../../helpers/storage'; | ||||
import withAppStateContext from './app-state/withAppStateContext'; | import withAppStateContext from './app-state/withAppStateContext'; | ||||
import withCurrentUserContext from './current-user/withCurrentUserContext'; | import withCurrentUserContext from './current-user/withCurrentUserContext'; | ||||
const LicensePromptModal = lazyLoadComponent( | |||||
() => import('../../apps/marketplace/components/LicensePromptModal'), | |||||
'LicensePromptModal' | |||||
); | |||||
interface StateProps { | interface StateProps { | ||||
currentUser: CurrentUser; | currentUser: CurrentUser; | ||||
} | } |
import { screen } from '@testing-library/react'; | import { screen } from '@testing-library/react'; | ||||
import React from 'react'; | import React from 'react'; | ||||
import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../helpers/globalMessages'; | import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../helpers/globalMessages'; | ||||
import { renderComponentApp } from '../../../helpers/testReactTestingUtils'; | |||||
import { renderApp } from '../../../helpers/testReactTestingUtils'; | |||||
function NullComponent() { | function NullComponent() { | ||||
return null; | return null; | ||||
jest.useFakeTimers(); | jest.useFakeTimers(); | ||||
// we render anything, the GlobalMessageContainer is rendered independently from routing | // we render anything, the GlobalMessageContainer is rendered independently from routing | ||||
renderComponentApp('sonarqube', <NullComponent />); | |||||
renderApp('sonarqube', <NullComponent />); | |||||
addGlobalErrorMessage('This is an error'); | addGlobalErrorMessage('This is an error'); | ||||
addGlobalSuccessMessage('This was a triumph!'); | addGlobalSuccessMessage('This was a triumph!'); |
exports[`should render correctly: default 1`] = ` | exports[`should render correctly: default 1`] = ` | ||||
<Fragment> | <Fragment> | ||||
<LazyComponentWrapper /> | |||||
<withRouter(withAppStateContext(PageTracker)) /> | |||||
<Outlet /> | <Outlet /> | ||||
<KeyboardShortcutsModal /> | <KeyboardShortcutsModal /> | ||||
</Fragment> | </Fragment> | ||||
exports[`should render correctly: with gravatar 1`] = ` | exports[`should render correctly: with gravatar 1`] = ` | ||||
<Fragment> | <Fragment> | ||||
<LazyComponentWrapper> | |||||
<withRouter(withAppStateContext(PageTracker))> | |||||
<link | <link | ||||
href="http://example.com" | href="http://example.com" | ||||
rel="preconnect" | rel="preconnect" | ||||
/> | /> | ||||
</LazyComponentWrapper> | |||||
</withRouter(withAppStateContext(PageTracker))> | |||||
<Outlet /> | <Outlet /> | ||||
<KeyboardShortcutsModal /> | <KeyboardShortcutsModal /> | ||||
</Fragment> | </Fragment> |
import { screen } from '@testing-library/react'; | import { screen } from '@testing-library/react'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockAppState } from '../../../../helpers/testMocks'; | import { mockAppState } from '../../../../helpers/testMocks'; | ||||
import { renderComponentApp } from '../../../../helpers/testReactTestingUtils'; | |||||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||||
import { Extension } from '../../../../types/types'; | import { Extension } from '../../../../types/types'; | ||||
import GlobalPageExtension, { GlobalPageExtensionProps } from '../GlobalPageExtension'; | import GlobalPageExtension, { GlobalPageExtensionProps } from '../GlobalPageExtension'; | ||||
globalPages: Extension[] = [], | globalPages: Extension[] = [], | ||||
params?: GlobalPageExtensionProps['params'] | 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 | |||||
}); | |||||
} | } |
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||
import { lazyLoadComponent } from '../../../../components/lazyLoadComponent'; | |||||
import AnalysisWarningsModal from '../../../../components/common/AnalysisWarningsModal'; | |||||
import { Alert } from '../../../../components/ui/Alert'; | import { Alert } from '../../../../components/ui/Alert'; | ||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { TaskWarning } from '../../../../types/tasks'; | import { TaskWarning } from '../../../../types/tasks'; | ||||
const AnalysisWarningsModal = lazyLoadComponent( | |||||
() => import('../../../../components/common/AnalysisWarningsModal'), | |||||
'AnalysisWarningsModal' | |||||
); | |||||
interface Props { | interface Props { | ||||
componentKey: string; | componentKey: string; | ||||
isBranch: boolean; | isBranch: boolean; |
} | } | ||||
/> | /> | ||||
</Alert> | </Alert> | ||||
<AnalysisWarningsModal | |||||
<withCurrentUserContext(AnalysisWarningsModal) | |||||
componentKey="foo" | componentKey="foo" | ||||
onClose={[Function]} | onClose={[Function]} | ||||
onWarningDismiss={[MockFunction]} | onWarningDismiss={[MockFunction]} |
import SearchBox from '../../../components/controls/SearchBox'; | import SearchBox from '../../../components/controls/SearchBox'; | ||||
import { Router, withRouter } from '../../../components/hoc/withRouter'; | import { Router, withRouter } from '../../../components/hoc/withRouter'; | ||||
import ClockIcon from '../../../components/icons/ClockIcon'; | import ClockIcon from '../../../components/icons/ClockIcon'; | ||||
import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; | |||||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | ||||
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; | import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; | ||||
import { KeyboardKeys } from '../../../helpers/keycodes'; | import { KeyboardKeys } from '../../../helpers/keycodes'; | ||||
import { Dict } from '../../../types/types'; | import { Dict } from '../../../types/types'; | ||||
import RecentHistory from '../RecentHistory'; | import RecentHistory from '../RecentHistory'; | ||||
import './Search.css'; | import './Search.css'; | ||||
import SearchResult from './SearchResult'; | |||||
import SearchResults from './SearchResults'; | |||||
import { ComponentResult, More, Results, sortQualifiers } from './utils'; | import { ComponentResult, More, Results, sortQualifiers } from './utils'; | ||||
const SearchResults = lazyLoadComponent(() => import('./SearchResults')); | |||||
const SearchResult = lazyLoadComponent(() => import('./SearchResult')); | |||||
interface Props { | interface Props { | ||||
router: Router; | router: Router; | ||||
} | } | ||||
interface State { | interface State { | ||||
loading: boolean; | loading: boolean; | ||||
loadingMore?: string; | loadingMore?: string; |
import UserTokensMock from '../../../api/mocks/UserTokensMock'; | import UserTokensMock from '../../../api/mocks/UserTokensMock'; | ||||
import { mockUserToken } from '../../../helpers/mocks/token'; | import { mockUserToken } from '../../../helpers/mocks/token'; | ||||
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; | import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; | ||||
import { renderApp } from '../../../helpers/testReactTestingUtils'; | |||||
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; | |||||
import { Permissions } from '../../../types/permissions'; | import { Permissions } from '../../../types/permissions'; | ||||
import { TokenType } from '../../../types/token'; | import { TokenType } from '../../../types/token'; | ||||
import { CurrentUser } from '../../../types/users'; | import { CurrentUser } from '../../../types/users'; | ||||
} | } | ||||
function renderAccountApp(currentUser: CurrentUser, navigateTo?: string) { | function renderAccountApp(currentUser: CurrentUser, navigateTo?: string) { | ||||
renderApp('account', routes, { currentUser, navigateTo }); | |||||
renderAppRoutes('account', routes, { currentUser, navigateTo }); | |||||
} | } |
import { screen } from '@testing-library/react'; | import { screen } from '@testing-library/react'; | ||||
import userEvent from '@testing-library/user-event'; | import userEvent from '@testing-library/user-event'; | ||||
import AuditLogsServiceMock from '../../../../api/mocks/AuditLogsServiceMock'; | import AuditLogsServiceMock from '../../../../api/mocks/AuditLogsServiceMock'; | ||||
import { renderAdminApp } from '../../../../helpers/testReactTestingUtils'; | |||||
import { renderAppWithAdminContext } from '../../../../helpers/testReactTestingUtils'; | |||||
import { AdminPageExtension } from '../../../../types/extension'; | import { AdminPageExtension } from '../../../../types/extension'; | ||||
import routes from '../../routes'; | import routes from '../../routes'; | ||||
}); | }); | ||||
function renderAuditLogs() { | function renderAuditLogs() { | ||||
renderAdminApp('admin/audit', routes, {}, { adminPages: extensions }); | |||||
renderAppWithAdminContext('admin/audit', routes, {}, { adminPages: extensions }); | |||||
} | } |
import userEvent from '@testing-library/user-event'; | import userEvent from '@testing-library/user-event'; | ||||
import { UserEvent } from '@testing-library/user-event/dist/types/setup'; | import { UserEvent } from '@testing-library/user-event/dist/types/setup'; | ||||
import ComputeEngineServiceMock from '../../../api/mocks/ComputeEngineServiceMock'; | import ComputeEngineServiceMock from '../../../api/mocks/ComputeEngineServiceMock'; | ||||
import { renderAdminApp } from '../../../helpers/testReactTestingUtils'; | |||||
import { renderAppWithAdminContext } from '../../../helpers/testReactTestingUtils'; | |||||
import { TaskStatuses, TaskTypes } from '../../../types/tasks'; | import { TaskStatuses, TaskTypes } from '../../../types/tasks'; | ||||
import routes from '../routes'; | import routes from '../routes'; | ||||
} | } | ||||
function renderGlobalBackgroundTasksApp() { | function renderGlobalBackgroundTasksApp() { | ||||
renderAdminApp('admin/background_tasks', routes, {}); | |||||
renderAppWithAdminContext('admin/background_tasks', routes, {}); | |||||
} | } |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import AnalysisWarningsModal from '../../../components/common/AnalysisWarningsModal'; | |||||
import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; | import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; | ||||
import ConfirmModal from '../../../components/controls/ConfirmModal'; | import ConfirmModal from '../../../components/controls/ConfirmModal'; | ||||
import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; | |||||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | import { translate, translateWithParameters } from '../../../helpers/l10n'; | ||||
import { Task, TaskStatuses } from '../../../types/tasks'; | import { Task, TaskStatuses } from '../../../types/tasks'; | ||||
import ScannerContext from './ScannerContext'; | import ScannerContext from './ScannerContext'; | ||||
import Stacktrace from './Stacktrace'; | import Stacktrace from './Stacktrace'; | ||||
const AnalysisWarningsModal = lazyLoadComponent( | |||||
() => import('../../../components/common/AnalysisWarningsModal'), | |||||
'AnalysisWarningsModal' | |||||
); | |||||
interface Props { | interface Props { | ||||
component?: unknown; | component?: unknown; | ||||
onCancelTask: (task: Task) => Promise<void>; | onCancelTask: (task: Task) => Promise<void>; |
it('shows warnings', () => { | it('shows warnings', () => { | ||||
const wrapper = shallowRender({ warningCount: 2 }); | const wrapper = shallowRender({ warningCount: 2 }); | ||||
click(wrapper.find('.js-task-show-warnings')); | 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(); | 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']>) { | function shallowRender(fields?: Partial<Task>, props?: Partial<TaskActions['props']>) { |
`; | `; | ||||
exports[`shows warnings 1`] = ` | exports[`shows warnings 1`] = ` | ||||
<AnalysisWarningsModal | |||||
<withCurrentUserContext(AnalysisWarningsModal) | |||||
componentKey="foo" | componentKey="foo" | ||||
onClose={[Function]} | onClose={[Function]} | ||||
taskId="AXR8jg_0mF2ZsYr8Wzs2" | taskId="AXR8jg_0mF2ZsYr8Wzs2" |
/* | |||||
* 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() }); | |||||
} |
/* | |||||
* 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} | |||||
/> | |||||
); | |||||
} |
// 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> | |||||
`; |
import userEvent from '@testing-library/user-event'; | import userEvent from '@testing-library/user-event'; | ||||
import CodingRulesMock from '../../../api/mocks/CodingRulesMock'; | import CodingRulesMock from '../../../api/mocks/CodingRulesMock'; | ||||
import { mockLoggedInUser } from '../../../helpers/testMocks'; | import { mockLoggedInUser } from '../../../helpers/testMocks'; | ||||
import { renderApp } from '../../../helpers/testReactTestingUtils'; | |||||
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; | |||||
import { CurrentUser } from '../../../types/users'; | import { CurrentUser } from '../../../types/users'; | ||||
import routes from '../routes'; | import routes from '../routes'; | ||||
}); | }); | ||||
function renderCodingRulesApp(currentUser?: CurrentUser, navigateTo?: string) { | function renderCodingRulesApp(currentUser?: CurrentUser, navigateTo?: string) { | ||||
renderApp('coding_rules', routes, { | |||||
renderAppRoutes('coding_rules', routes, { | |||||
navigateTo, | navigateTo, | ||||
currentUser, | currentUser, | ||||
languages: { | languages: { |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { screen } from '@testing-library/react'; | import { screen } from '@testing-library/react'; | ||||
import { renderApp } from '../../../helpers/testReactTestingUtils'; | |||||
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; | |||||
import routes from '../routes'; | import routes from '../routes'; | ||||
it('should redirect old history route', () => { | it('should redirect old history route', () => { | ||||
}); | }); | ||||
function renderMeasuresApp(navigateTo?: string) { | function renderMeasuresApp(navigateTo?: string) { | ||||
renderApp('component_measures', routes, { navigateTo }); | |||||
renderAppRoutes('component_measures', routes, { navigateTo }); | |||||
} | } |
> | > | ||||
<SourceViewer | <SourceViewer | ||||
component="foo:src/index.tsx" | component="foo:src/index.tsx" | ||||
displayAllIssues={false} | |||||
displayIssueLocationsCount={true} | |||||
displayIssueLocationsLink={true} | |||||
displayLocationMarkers={true} | |||||
metricKey="bugs" | metricKey="bugs" | ||||
scroll={[Function]} | scroll={[Function]} | ||||
/> | /> |
> | > | ||||
<SourceViewer | <SourceViewer | ||||
component="foo:src/index.tsx" | component="foo:src/index.tsx" | ||||
displayAllIssues={false} | |||||
displayIssueLocationsCount={true} | |||||
displayIssueLocationsLink={true} | |||||
displayLocationMarkers={true} | |||||
/> | /> | ||||
</div> | </div> | ||||
</div> | </div> |
import React from 'react'; | import React from 'react'; | ||||
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; | import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; | ||||
import { renderOwaspTop102021Category } from '../../../helpers/security-standard'; | import { renderOwaspTop102021Category } from '../../../helpers/security-standard'; | ||||
import { renderApp, renderComponentApp } from '../../../helpers/testReactTestingUtils'; | |||||
import { renderApp, renderAppRoutes } from '../../../helpers/testReactTestingUtils'; | |||||
import { IssueType } from '../../../types/issues'; | import { IssueType } from '../../../types/issues'; | ||||
import AppContainer from '../components/AppContainer'; | |||||
import IssuesApp from '../components/IssuesApp'; | |||||
import { projectIssuesRoutes } from '../routes'; | import { projectIssuesRoutes } from '../routes'; | ||||
jest.mock('../../../api/issues'); | jest.mock('../../../api/issues'); | ||||
}); | }); | ||||
function renderIssueApp() { | function renderIssueApp() { | ||||
renderComponentApp('project/issues', <AppContainer />); | |||||
renderApp('project/issues', <IssuesApp />); | |||||
} | } | ||||
function renderProjectIssuesApp(navigateTo?: string) { | function renderProjectIssuesApp(navigateTo?: string) { | ||||
renderApp('project/issues', projectIssuesRoutes, { navigateTo }); | |||||
renderAppRoutes('project/issues', projectIssuesRoutes, { navigateTo }); | |||||
} | } |
/* | |||||
* 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 | |||||
); |
import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||
import { searchIssues } from '../../../api/issues'; | import { searchIssues } from '../../../api/issues'; | ||||
import { getRuleDetails } from '../../../api/rules'; | import { getRuleDetails } from '../../../api/rules'; | ||||
import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; | |||||
import withComponentContext from '../../../app/components/componentContext/withComponentContext'; | 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 A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; | ||||
import EmptySearch from '../../../components/common/EmptySearch'; | import EmptySearch from '../../../components/common/EmptySearch'; | ||||
import FiltersHeader from '../../../components/common/FiltersHeader'; | import FiltersHeader from '../../../components/common/FiltersHeader'; | ||||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | import HelpTooltip from '../../../components/controls/HelpTooltip'; | ||||
import ListFooter from '../../../components/controls/ListFooter'; | import ListFooter from '../../../components/controls/ListFooter'; | ||||
import Suggestions from '../../../components/embed-docs-modal/Suggestions'; | 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 '../../../components/search-navigator.css'; | ||||
import { Alert } from '../../../components/ui/Alert'; | import { Alert } from '../../../components/ui/Alert'; | ||||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | ||||
align-items: center; | align-items: center; | ||||
`; | `; | ||||
export default withComponentContext(App); | |||||
export default withIndexationGuard( | |||||
withRouter(withCurrentUserContext(withBranchStatusActions(withComponentContext(App)))), | |||||
PageContext.Issues | |||||
); |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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> | |||||
); | |||||
} | |||||
} |
/* | |||||
* 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> | |||||
); | |||||
} | |||||
} |
} from '../../../../helpers/mocks/sources'; | } from '../../../../helpers/mocks/sources'; | ||||
import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks'; | import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks'; | ||||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | import { waitAndUpdate } from '../../../../helpers/testUtils'; | ||||
import CrossComponentSourceViewerWrapper from '../CrossComponentSourceViewerWrapper'; | |||||
import CrossComponentSourceViewer from '../CrossComponentSourceViewer'; | |||||
jest.mock('../../../../api/issues', () => { | jest.mock('../../../../api/issues', () => { | ||||
const { mockSnippetsByComponent } = jest.requireActual('../../../../helpers/mocks/sources'); | const { mockSnippetsByComponent } = jest.requireActual('../../../../helpers/mocks/sources'); | ||||
).toMatchSnapshot(); | ).toMatchSnapshot(); | ||||
}); | }); | ||||
function shallowRender(props: Partial<CrossComponentSourceViewerWrapper['props']> = {}) { | |||||
return shallow<CrossComponentSourceViewerWrapper>( | |||||
<CrossComponentSourceViewerWrapper | |||||
function shallowRender(props: Partial<CrossComponentSourceViewer['props']> = {}) { | |||||
return shallow<CrossComponentSourceViewer>( | |||||
<CrossComponentSourceViewer | |||||
branchLike={undefined} | branchLike={undefined} | ||||
highlightedLocationMessage={undefined} | highlightedLocationMessage={undefined} | ||||
issue={mockIssue(true, { key: '1' })} | issue={mockIssue(true, { key: '1' })} |
import { Route, useNavigate, useSearchParams } from 'react-router-dom'; | import { Route, useNavigate, useSearchParams } from 'react-router-dom'; | ||||
import { omitNil } from '../../helpers/request'; | import { omitNil } from '../../helpers/request'; | ||||
import { IssueType } from '../../types/issues'; | 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 = () => ( | export const projectIssuesRoutes = () => ( | ||||
<Route path="project/issues" element={<IssuesNavigate />} /> | <Route path="project/issues" element={<IssuesNavigate />} /> | ||||
} | } | ||||
}, [navigate, searchParams, setSearchParams]); | }, [navigate, searchParams, setSearchParams]); | ||||
return <AppContainer />; | |||||
return <IssuesApp />; | |||||
} | } |
import tooltipDE from 'Docs/tooltips/editions/developer.md'; | import tooltipDE from 'Docs/tooltips/editions/developer.md'; | ||||
import tooltipEE from 'Docs/tooltips/editions/enterprise.md'; | import tooltipEE from 'Docs/tooltips/editions/enterprise.md'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; | |||||
import DocMarkdownBlock from '../../../components/docs/DocMarkdownBlock'; | |||||
import { getEditionUrl } from '../../../helpers/editions'; | import { getEditionUrl } from '../../../helpers/editions'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { Edition, EditionKey } from '../../../types/editions'; | import { Edition, EditionKey } from '../../../types/editions'; | ||||
const DocMarkdownBlock = lazyLoadComponent( | |||||
() => import('../../../components/docs/DocMarkdownBlock'), | |||||
'DocMarkdownBlock' | |||||
); | |||||
interface Props { | interface Props { | ||||
currentEdition?: EditionKey; | currentEdition?: EditionKey; | ||||
edition: Edition; | edition: Edition; |
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | ||||
import withComponentContext from '../../../app/components/componentContext/withComponentContext'; | import withComponentContext from '../../../app/components/componentContext/withComponentContext'; | ||||
import Suggestions from '../../../components/embed-docs-modal/Suggestions'; | import Suggestions from '../../../components/embed-docs-modal/Suggestions'; | ||||
import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; | |||||
import { isPullRequest } from '../../../helpers/branch-like'; | import { isPullRequest } from '../../../helpers/branch-like'; | ||||
import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; | import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; | ||||
import { AppState } from '../../../types/appstate'; | import { AppState } from '../../../types/appstate'; | ||||
import { isPortfolioLike } from '../../../types/component'; | import { isPortfolioLike } from '../../../types/component'; | ||||
import { Component } from '../../../types/types'; | import { Component } from '../../../types/types'; | ||||
import BranchOverview from '../branches/BranchOverview'; | 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 { | interface Props { | ||||
appState: AppState; | appState: AppState; |
import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext'; | import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext'; | ||||
import { getActivityGraph } from '../../../../components/activity-graph/utils'; | import { getActivityGraph } from '../../../../components/activity-graph/utils'; | ||||
import { mockComponent } from '../../../../helpers/mocks/component'; | import { mockComponent } from '../../../../helpers/mocks/component'; | ||||
import { renderComponentApp } from '../../../../helpers/testReactTestingUtils'; | |||||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||||
import { ComponentQualifier } from '../../../../types/component'; | import { ComponentQualifier } from '../../../../types/component'; | ||||
import { Component } from '../../../../types/types'; | import { Component } from '../../../../types/types'; | ||||
import ProjectActivityAppContainer from '../ProjectActivityAppContainer'; | import ProjectActivityAppContainer from '../ProjectActivityAppContainer'; | ||||
}) | }) | ||||
} | } | ||||
) { | ) { | ||||
return renderComponentApp( | |||||
return renderApp( | |||||
'project/activity', | 'project/activity', | ||||
<ComponentContext.Provider | <ComponentContext.Provider | ||||
value={{ | value={{ |
import * as React from 'react'; | import * as React from 'react'; | ||||
import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; | import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; | ||||
import { mockComponent } from '../../../helpers/mocks/component'; | import { mockComponent } from '../../../helpers/mocks/component'; | ||||
import { renderComponentApp } from '../../../helpers/testReactTestingUtils'; | |||||
import { renderApp } from '../../../helpers/testReactTestingUtils'; | |||||
import { ComponentContextShape } from '../../../types/component'; | import { ComponentContextShape } from '../../../types/component'; | ||||
import { Component } from '../../../types/types'; | import { Component } from '../../../types/types'; | ||||
import App from '../App'; | import App from '../App'; | ||||
}); | }); | ||||
function renderProjectDeletionApp(component?: Component) { | function renderProjectDeletionApp(component?: Component) { | ||||
renderComponentApp( | |||||
renderApp( | |||||
'project-delete', | 'project-delete', | ||||
<ComponentContext.Provider value={{ component } as ComponentContextShape}> | <ComponentContext.Provider value={{ component } as ComponentContextShape}> | ||||
<App /> | <App /> |
/* | |||||
* 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')); |
import { hasGlobalPermission } from '../../../helpers/users'; | import { hasGlobalPermission } from '../../../helpers/users'; | ||||
import { CurrentUser, isLoggedIn } from '../../../types/users'; | import { CurrentUser, isLoggedIn } from '../../../types/users'; | ||||
import { PROJECTS_ALL, PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE } from '../utils'; | import { PROJECTS_ALL, PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE } from '../utils'; | ||||
import AllProjectsContainer from './AllProjectsContainer'; | |||||
import AllProjects from './AllProjects'; | |||||
export interface DefaultPageSelectorProps { | export interface DefaultPageSelectorProps { | ||||
currentUser: CurrentUser; | currentUser: CurrentUser; | ||||
return null; | return null; | ||||
} | } | ||||
return <AllProjectsContainer isFavorite={false} />; | |||||
return <AllProjects isFavorite={false} />; | |||||
} | } | ||||
export default withCurrentUserContext(DefaultPageSelector); | export default withCurrentUserContext(DefaultPageSelector); |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import AllProjectsContainer from './AllProjectsContainer'; | |||||
import AllProjects from './AllProjects'; | |||||
export default function FavoriteProjectsContainer(props: any) { | export default function FavoriteProjectsContainer(props: any) { | ||||
return <AllProjectsContainer isFavorite={true} {...props} />; | |||||
return <AllProjects isFavorite={true} {...props} />; | |||||
} | } |
import { DefaultPageSelector } from '../DefaultPageSelector'; | import { DefaultPageSelector } from '../DefaultPageSelector'; | ||||
jest.mock( | jest.mock( | ||||
'../AllProjectsContainer', | |||||
'../AllProjects', | |||||
() => | () => | ||||
// eslint-disable-next-line | // eslint-disable-next-line | ||||
function AllProjectsContainer() { | |||||
function AllProjects() { | |||||
return <div>All Projects</div>; | return <div>All Projects</div>; | ||||
} | } | ||||
); | ); |
import userEvent from '@testing-library/user-event'; | import userEvent from '@testing-library/user-event'; | ||||
import { getComponents, SearchProjectsParameters } from '../../../api/components'; | import { getComponents, SearchProjectsParameters } from '../../../api/components'; | ||||
import PermissionTemplateServiceMock from '../../../api/mocks/PermissionTemplateServiceMock'; | import PermissionTemplateServiceMock from '../../../api/mocks/PermissionTemplateServiceMock'; | ||||
import { renderAdminApp } from '../../../helpers/testReactTestingUtils'; | |||||
import { renderAppWithAdminContext } from '../../../helpers/testReactTestingUtils'; | |||||
import { ComponentQualifier, Visibility } from '../../../types/component'; | import { ComponentQualifier, Visibility } from '../../../types/component'; | ||||
import routes from '../routes'; | import routes from '../routes'; | ||||
} | } | ||||
function renderGlobalBackgroundTasksApp() { | function renderGlobalBackgroundTasksApp() { | ||||
renderAdminApp('admin/projects_management', routes, {}); | |||||
renderAppWithAdminContext('admin/projects_management', routes, {}); | |||||
} | } |
import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock'; | import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock'; | ||||
import { searchProjects, searchUsers } from '../../../../api/quality-gates'; | import { searchProjects, searchUsers } from '../../../../api/quality-gates'; | ||||
import { mockAppState } from '../../../../helpers/testMocks'; | import { mockAppState } from '../../../../helpers/testMocks'; | ||||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||||
import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; | |||||
import { AppState } from '../../../../types/appstate'; | import { AppState } from '../../../../types/appstate'; | ||||
import routes from '../../routes'; | import routes from '../../routes'; | ||||
}); | }); | ||||
function renderQualityGateApp(appState?: AppState) { | function renderQualityGateApp(appState?: AppState) { | ||||
renderApp('quality_gates', routes, { appState }); | |||||
renderAppRoutes('quality_gates', routes, { appState }); | |||||
} | } |
import { Profile } from '../../../api/quality-profiles'; | import { Profile } from '../../../api/quality-profiles'; | ||||
import { getRuleDetails } from '../../../api/rules'; | import { getRuleDetails } from '../../../api/rules'; | ||||
import { Button } from '../../../components/controls/buttons'; | import { Button } from '../../../components/controls/buttons'; | ||||
import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; | |||||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { RuleDetails } from '../../../types/types'; | import { RuleDetails } from '../../../types/types'; | ||||
const ActivationFormModal = lazyLoadComponent( | |||||
() => import('../../coding-rules/components/ActivationFormModal'), | |||||
'ActivationFormModal' | |||||
); | |||||
import ActivationFormModal from '../../coding-rules/components/ActivationFormModal'; | |||||
interface Props { | interface Props { | ||||
onDone: () => Promise<void>; | onDone: () => Promise<void>; |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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> | |||||
); | |||||
} | |||||
} |
/* | |||||
* 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> | |||||
); | |||||
} | |||||
} |
import userEvent from '@testing-library/user-event'; | import userEvent from '@testing-library/user-event'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { SourceViewerServiceMock } from '../../../api/mocks/SourceViewerServiceMock'; | import { SourceViewerServiceMock } from '../../../api/mocks/SourceViewerServiceMock'; | ||||
import { HttpStatus } from '../../../helpers/request'; | |||||
import { mockIssue } from '../../../helpers/testMocks'; | import { mockIssue } from '../../../helpers/testMocks'; | ||||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | import { renderComponent } from '../../../helpers/testReactTestingUtils'; | ||||
import SourceViewer from '../SourceViewer'; | import SourceViewer from '../SourceViewer'; | ||||
import SourceViewerBase from '../SourceViewerBase'; | |||||
jest.mock('../../../api/components'); | jest.mock('../../../api/components'); | ||||
jest.mock('../../../api/issues'); | |||||
jest.mock('../helpers/lines', () => { | jest.mock('../helpers/lines', () => { | ||||
const lines = jest.requireActual('../helpers/lines'); | const lines = jest.requireActual('../helpers/lines'); | ||||
return { | return { | ||||
const handler = new SourceViewerServiceMock(); | const handler = new SourceViewerServiceMock(); | ||||
beforeEach(() => { | |||||
handler.reset(); | |||||
}); | |||||
it('should show a permalink on line number', async () => { | it('should show a permalink on line number', async () => { | ||||
const user = userEvent.setup(); | const user = userEvent.setup(); | ||||
renderSourceViewer(); | renderSourceViewer(); | ||||
expect(await screen.findByRole('row', { name: 'First Issue' })).toBeInTheDocument(); | 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 () => { | it('should load line when looking arround unloaded line', async () => { | ||||
const { rerender } = renderSourceViewer({ | const { rerender } = renderSourceViewer({ | ||||
aroundLine: 50, | aroundLine: 50, | ||||
expect(duplicateLine.queryByRole('link', { name: 'test2.js' })).not.toBeInTheDocument(); | 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)); | return renderComponent(getSourceViewerUi(override)); | ||||
} | } | ||||
function getSourceViewerUi(override?: Partial<SourceViewerBase['props']>) { | |||||
function getSourceViewerUi(override?: Partial<SourceViewer['props']>) { | |||||
return ( | return ( | ||||
<SourceViewer | <SourceViewer | ||||
aroundLine={1} | aroundLine={1} |
import { mockIssue } from '../../../helpers/testMocks'; | import { mockIssue } from '../../../helpers/testMocks'; | ||||
import { waitAndUpdate } from '../../../helpers/testUtils'; | import { waitAndUpdate } from '../../../helpers/testUtils'; | ||||
import defaultLoadIssues from '../helpers/loadIssues'; | import defaultLoadIssues from '../helpers/loadIssues'; | ||||
import SourceViewerBase from '../SourceViewerBase'; | |||||
import SourceViewer from '../SourceViewer'; | |||||
jest.mock('../helpers/loadIssues', () => jest.fn().mockRejectedValue({})); | jest.mock('../helpers/loadIssues', () => jest.fn().mockRejectedValue({})); | ||||
expect(wrapper.instance().isLineOutsideOfRange(12)).toBe(true); | 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} /> | |||||
); | ); | ||||
} | } |
// 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> | |||||
`; |
/* | |||||
* 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(); | |||||
}); |
import { addMonths, setMonth, setYear, subMonths } from 'date-fns'; | import { addMonths, setMonth, setYear, subMonths } from 'date-fns'; | ||||
import { range } from 'lodash'; | import { range } from 'lodash'; | ||||
import * as React from 'react'; | 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 { injectIntl, WrappedComponentProps } from 'react-intl'; | ||||
import { ButtonIcon, ClearButton } from '../../components/controls/buttons'; | import { ButtonIcon, ClearButton } from '../../components/controls/buttons'; | ||||
import OutsideClickHandler from '../../components/controls/OutsideClickHandler'; | import OutsideClickHandler from '../../components/controls/OutsideClickHandler'; | ||||
import ChevronLeftIcon from '../../components/icons/ChevronLeftIcon'; | import ChevronLeftIcon from '../../components/icons/ChevronLeftIcon'; | ||||
import ChevronRightIcon from '../../components/icons/ChevronRightIcon'; | import ChevronRightIcon from '../../components/icons/ChevronRightIcon'; | ||||
import { getShortMonthName, getShortWeekDayName, getWeekDayName } from '../../helpers/l10n'; | import { getShortMonthName, getShortWeekDayName, getWeekDayName } from '../../helpers/l10n'; | ||||
import { lazyLoadComponent } from '../lazyLoadComponent'; | |||||
import './DayPicker.css'; | import './DayPicker.css'; | ||||
import Select from './Select'; | import Select from './Select'; | ||||
import './styles.css'; | import './styles.css'; | ||||
const DayPicker = lazyLoadComponent(() => import('react-day-picker'), 'DayPicker'); | |||||
interface Props { | interface Props { | ||||
className?: string; | className?: string; | ||||
currentMonth?: Date; | currentMonth?: Date; |
</ButtonIcon> | </ButtonIcon> | ||||
</nav> | </nav> | ||||
<DayPicker | <DayPicker | ||||
canChangeMonth={true} | |||||
captionElement={<NullComponent />} | 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={ | disabledDays={ | ||||
Object { | Object { | ||||
"after": 2018-02-05T00:00:00.000Z, | "after": 2018-02-05T00:00:00.000Z, | ||||
"before": 2018-01-17T00:00:00.000Z, | "before": 2018-01-17T00:00:00.000Z, | ||||
} | } | ||||
} | } | ||||
enableOutsideDaysClick={true} | |||||
firstDayOfWeek={1} | 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} | month={2018-01-17T00:00:00.000Z} | ||||
navbarElement={<NullComponent />} | navbarElement={<NullComponent />} | ||||
numberOfMonths={1} | |||||
onDayClick={[Function]} | onDayClick={[Function]} | ||||
onDayMouseEnter={[Function]} | onDayMouseEnter={[Function]} | ||||
pagedNavigation={false} | |||||
renderDay={[Function]} | |||||
renderWeek={[Function]} | |||||
reverseMonths={false} | |||||
selectedDays={ | selectedDays={ | ||||
Array [ | Array [ | ||||
2018-01-17T00:00:00.000Z, | 2018-01-17T00:00:00.000Z, | ||||
] | ] | ||||
} | } | ||||
showOutsideDays={false} | |||||
showWeekDays={true} | |||||
showWeekNumbers={false} | |||||
tabIndex={0} | |||||
weekdayElement={<Weekday />} | |||||
weekdaysLong={ | weekdaysLong={ | ||||
Array [ | Array [ | ||||
"Sunday", | "Sunday", |
import { ButtonLink } from '../controls/buttons'; | import { ButtonLink } from '../controls/buttons'; | ||||
import Toggler from '../controls/Toggler'; | import Toggler from '../controls/Toggler'; | ||||
import HelpIcon from '../icons/HelpIcon'; | import HelpIcon from '../icons/HelpIcon'; | ||||
import { lazyLoadComponent } from '../lazyLoadComponent'; | |||||
const EmbedDocsPopup = lazyLoadComponent(() => import('./EmbedDocsPopup')); | |||||
import EmbedDocsPopup from './EmbedDocsPopup'; | |||||
interface State { | interface State { | ||||
helpOpen: boolean; | helpOpen: boolean; |
/* | |||||
* 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; | |||||
} | |||||
} |
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { colors } from '../../app/theme'; | 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 }; | const SIZE_TO_WIDTH_MAPPING = { small: 16, normal: 24, big: 40, huge: 60 }; | ||||
import { getRulesApp } from '../../api/rules'; | import { getRulesApp } from '../../api/rules'; | ||||
import { get, save } from '../../helpers/storage'; | import { get, save } from '../../helpers/storage'; | ||||
import { Dict } from '../../types/types'; | import { Dict } from '../../types/types'; | ||||
import { lazyLoadComponent } from '../lazyLoadComponent'; | |||||
import { ComponentDescriptor, RuleDescriptor, WorkspaceContext } from './context'; | import { ComponentDescriptor, RuleDescriptor, WorkspaceContext } from './context'; | ||||
import './styles.css'; | import './styles.css'; | ||||
import WorkspaceComponentViewer from './WorkspaceComponentViewer'; | |||||
import WorkspaceNav from './WorkspaceNav'; | |||||
import WorkspacePortal from './WorkspacePortal'; | import WorkspacePortal from './WorkspacePortal'; | ||||
import WorkspaceRuleViewer from './WorkspaceRuleViewer'; | |||||
const WORKSPACE = 'sonarqube-workspace'; | const WORKSPACE = 'sonarqube-workspace'; | ||||
const WorkspaceNav = lazyLoadComponent(() => import('./WorkspaceNav'), 'WorkspaceNav'); | |||||
const WorkspaceRuleViewer = lazyLoadComponent( | |||||
() => import('./WorkspaceRuleViewer'), | |||||
'WorkspaceRuleViewer' | |||||
); | |||||
const WorkspaceComponentViewer = lazyLoadComponent( | |||||
() => import('./WorkspaceComponentViewer'), | |||||
'WorkspaceComponentViewer' | |||||
); | |||||
interface State { | interface State { | ||||
components: ComponentDescriptor[]; | components: ComponentDescriptor[]; | ||||
externalRulesRepoNames: Dict<string>; | externalRulesRepoNames: Dict<string>; |
} | } | ||||
rules={Array []} | rules={Array []} | ||||
/> | /> | ||||
<WorkspaceComponentViewer | |||||
<withBranchStatusActions(WorkspaceComponentViewer) | |||||
component={ | component={ | ||||
Object { | Object { | ||||
"branchLike": Object { | "branchLike": Object { |
> | > | ||||
<SourceViewer | <SourceViewer | ||||
component="foo" | component="foo" | ||||
displayAllIssues={false} | |||||
displayIssueLocationsCount={true} | |||||
displayIssueLocationsLink={true} | |||||
displayLocationMarkers={true} | |||||
onIssueChange={[Function]} | onIssueChange={[Function]} | ||||
onLoaded={[Function]} | onLoaded={[Function]} | ||||
/> | /> |
jest.mock('../system', () => ({ | jest.mock('../system', () => ({ | ||||
getBaseUrl: () => '' | getBaseUrl: () => '' | ||||
})); | })); | ||||
jest.mock('../browser', () => ({ | |||||
IS_SSR: false | |||||
})); | |||||
const mockedUrls = require('../urls'); | const mockedUrls = require('../urls'); | ||||
expect(mockedUrls.getHostUrl()).toBe('http://localhost'); | 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', () => { | describe('searchParamsToQuery', () => { |
*/ | */ | ||||
import { EnhancedWindow } from '../types/browser'; | import { EnhancedWindow } from '../types/browser'; | ||||
export const IS_SSR = typeof window === 'undefined'; | |||||
export function getEnhancedWindow() { | export function getEnhancedWindow() { | ||||
return (window as unknown) as EnhancedWindow; | return (window as unknown) as EnhancedWindow; | ||||
} | } |
import { MemoryRouter, Outlet, parsePath, Route, Routes } from 'react-router-dom'; | import { MemoryRouter, Outlet, parsePath, Route, Routes } from 'react-router-dom'; | ||||
import AdminContext from '../app/components/AdminContext'; | import AdminContext from '../app/components/AdminContext'; | ||||
import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider'; | import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider'; | ||||
import { ComponentContext } from '../app/components/componentContext/ComponentContext'; | |||||
import CurrentUserContextProvider from '../app/components/current-user/CurrentUserContextProvider'; | import CurrentUserContextProvider from '../app/components/current-user/CurrentUserContextProvider'; | ||||
import GlobalMessagesContainer from '../app/components/GlobalMessagesContainer'; | import GlobalMessagesContainer from '../app/components/GlobalMessagesContainer'; | ||||
import IndexationContextProvider from '../app/components/indexation/IndexationContextProvider'; | import IndexationContextProvider from '../app/components/indexation/IndexationContextProvider'; | ||||
import { MetricsContext } from '../app/components/metrics/MetricsContext'; | import { MetricsContext } from '../app/components/metrics/MetricsContext'; | ||||
import { useLocation } from '../components/hoc/withRouter'; | import { useLocation } from '../components/hoc/withRouter'; | ||||
import { AppState } from '../types/appstate'; | import { AppState } from '../types/appstate'; | ||||
import { ComponentContextShape } from '../types/component'; | |||||
import { Dict, Extension, Languages, Metric, SysStatus } from '../types/types'; | import { Dict, Extension, Languages, Metric, SysStatus } from '../types/types'; | ||||
import { CurrentUser } from '../types/users'; | import { CurrentUser } from '../types/users'; | ||||
import { DEFAULT_METRICS } from './mocks/metrics'; | import { DEFAULT_METRICS } from './mocks/metrics'; | ||||
navigateTo?: string; | navigateTo?: string; | ||||
} | } | ||||
export function renderAdminApp( | |||||
export function renderAppWithAdminContext( | |||||
indexPath: string, | indexPath: string, | ||||
routes: () => JSX.Element, | routes: () => JSX.Element, | ||||
context: RenderContext = {}, | context: RenderContext = {}, | ||||
return render(component, { wrapper: Wrapper }); | 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, | indexPath: string, | ||||
component: JSX.Element, | component: JSX.Element, | ||||
context: RenderContext = {} | context: RenderContext = {} | ||||
return renderRoutedApp(<Route path={indexPath} element={component} />, indexPath, context); | return renderRoutedApp(<Route path={indexPath} element={component} />, indexPath, context); | ||||
} | } | ||||
export function renderApp( | |||||
export function renderAppRoutes( | |||||
indexPath: string, | indexPath: string, | ||||
routes: () => JSX.Element, | routes: () => JSX.Element, | ||||
context?: RenderContext | context?: RenderContext |
import { Dict, RawQuery } from '../types/types'; | import { Dict, RawQuery } from '../types/types'; | ||||
import { HomePage } from '../types/users'; | import { HomePage } from '../types/users'; | ||||
import { getBranchLikeQuery, isBranch, isMainBranch, isPullRequest } from './branch-like'; | import { getBranchLikeQuery, isBranch, isMainBranch, isPullRequest } from './branch-like'; | ||||
import { IS_SSR } from './browser'; | |||||
import { serializeOptionalBoolean } from './query'; | import { serializeOptionalBoolean } from './query'; | ||||
import { getBaseUrl } from './system'; | import { getBaseUrl } from './system'; | ||||
} | } | ||||
export function getHostUrl(): string { | export function getHostUrl(): string { | ||||
if (IS_SSR) { | |||||
throw new Error('No host url available on server side.'); | |||||
} | |||||
return window.location.origin + getBaseUrl(); | return window.location.origin + getBaseUrl(); | ||||
} | } | ||||