Kaynağa Gözat

SONAR-16475 Remove lazyLoading of frontend component

tags/9.6.0.59041
Mathieu Suen 1 yıl önce
ebeveyn
işleme
0a7d6f7245
62 değiştirilmiş dosya ile 1479 ekleme ve 1954 silme
  1. 168
    0
      server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts
  2. 45
    5
      server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts
  3. 1
    3
      server/sonar-web/src/main/js/app/components/App.tsx
  4. 1
    3
      server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx
  5. 1
    6
      server/sonar-web/src/main/js/app/components/StartupModal.tsx
  6. 2
    2
      server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.tsx
  7. 3
    3
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap
  8. 5
    9
      server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx
  9. 1
    6
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx
  10. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap
  11. 2
    5
      server/sonar-web/src/main/js/app/components/search/Search.tsx
  12. 2
    2
      server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
  13. 2
    2
      server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx
  14. 2
    2
      server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx
  15. 1
    6
      server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
  16. 3
    3
      server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/TaskActions-test.tsx
  17. 1
    1
      server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap
  18. 71
    0
      server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
  19. 0
    53
      server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx
  20. 0
    530
      server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap
  21. 2
    2
      server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
  22. 2
    2
      server/sonar-web/src/main/js/apps/component-measures/__tests__/MeasuresApp-it.tsx
  23. 4
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
  24. 4
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap
  25. 4
    4
      server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
  26. 0
    32
      server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
  27. 9
    2
      server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
  28. 277
    6
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
  29. 0
    296
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx
  30. 4
    4
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
  31. 0
    0
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap
  32. 3
    3
      server/sonar-web/src/main/js/apps/issues/routes.tsx
  33. 1
    6
      server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
  34. 2
    4
      server/sonar-web/src/main/js/apps/overview/components/App.tsx
  35. 2
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx
  36. 2
    2
      server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx
  37. 0
    22
      server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx
  38. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
  39. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx
  40. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx
  41. 2
    2
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
  42. 2
    2
      server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx
  43. 1
    6
      server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx
  44. 647
    6
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
  45. 0
    673
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
  46. 81
    3
      server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
  47. 4
    4
      server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx
  48. 0
    0
      server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap
  49. 0
    36
      server/sonar-web/src/main/js/components/__tests__/__snapshots__/lazyLoadComponent-test.tsx.snap
  50. 0
    65
      server/sonar-web/src/main/js/components/__tests__/lazyLoadComponent-test.tsx
  51. 1
    4
      server/sonar-web/src/main/js/components/controls/DateInput.tsx
  52. 65
    0
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap
  53. 1
    3
      server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
  54. 0
    73
      server/sonar-web/src/main/js/components/lazyLoadComponent.tsx
  55. 1
    6
      server/sonar-web/src/main/js/components/ui/CoverageRating.tsx
  56. 3
    11
      server/sonar-web/src/main/js/components/workspace/Workspace.tsx
  57. 1
    1
      server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap
  58. 4
    0
      server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap
  59. 0
    15
      server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
  60. 0
    2
      server/sonar-web/src/main/js/helpers/browser.ts
  61. 32
    3
      server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
  62. 0
    4
      server/sonar-web/src/main/js/helpers/urls.ts

+ 168
- 0
server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts Dosyayı Görüntüle

/*
* 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));
}
}

+ 45
- 5
server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts Dosyayı Görüntüle

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));
} }

+ 1
- 3
server/sonar-web/src/main/js/app/components/App.tsx Dosyayı Görüntüle

*/ */
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;

+ 1
- 3
server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx Dosyayı Görüntüle

*/ */
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 (

+ 1
- 6
server/sonar-web/src/main/js/app/components/StartupModal.tsx Dosyayı Görüntüle

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;
} }

+ 2
- 2
server/sonar-web/src/main/js/app/components/__tests__/GlobalMessagesContainer-it.tsx Dosyayı Görüntüle

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!');

+ 3
- 3
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap Dosyayı Görüntüle



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>

+ 5
- 9
server/sonar-web/src/main/js/app/components/extensions/__tests__/GlobalPageExtension-test.tsx Dosyayı Görüntüle

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
});
} }

+ 1
- 6
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx Dosyayı Görüntüle

*/ */
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;

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap Dosyayı Görüntüle

} }
/> />
</Alert> </Alert>
<AnalysisWarningsModal
<withCurrentUserContext(AnalysisWarningsModal)
componentKey="foo" componentKey="foo"
onClose={[Function]} onClose={[Function]}
onWarningDismiss={[MockFunction]} onWarningDismiss={[MockFunction]}

+ 2
- 5
server/sonar-web/src/main/js/app/components/search/Search.tsx Dosyayı Görüntüle

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;

+ 2
- 2
server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx Dosyayı Görüntüle

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 });
} }

+ 2
- 2
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx Dosyayı Görüntüle

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 });
} }

+ 2
- 2
server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx Dosyayı Görüntüle

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, {});
} }

+ 1
- 6
server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx Dosyayı Görüntüle

* 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>;

+ 3
- 3
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/TaskActions-test.tsx Dosyayı Görüntüle

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']>) {

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap Dosyayı Görüntüle

`; `;


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"

+ 71
- 0
server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts Dosyayı Görüntüle

/*
* 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() });
}

+ 0
- 53
server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx Dosyayı Görüntüle

/*
* 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}
/>
);
}

+ 0
- 530
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap Dosyayı Görüntüle

// 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>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts Dosyayı Görüntüle

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: {

+ 2
- 2
server/sonar-web/src/main/js/apps/component-measures/__tests__/MeasuresApp-it.tsx Dosyayı Görüntüle

* 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 });
} }

+ 4
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap Dosyayı Görüntüle

> >
<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]}
/> />

+ 4
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap Dosyayı Görüntüle

> >
<SourceViewer <SourceViewer
component="foo:src/index.tsx" component="foo:src/index.tsx"
displayAllIssues={false}
displayIssueLocationsCount={true}
displayIssueLocationsLink={true}
displayLocationMarkers={true}
/> />
</div> </div>
</div> </div>

+ 4
- 4
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx Dosyayı Görüntüle

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 });
} }

+ 0
- 32
server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx Dosyayı Görüntüle

/*
* 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
);

+ 9
- 2
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx Dosyayı Görüntüle

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
);

+ 277
- 6
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx Dosyayı Görüntüle

* 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>
);
}
}

+ 0
- 296
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx Dosyayı Görüntüle

/*
* 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>
);
}
}

server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx → server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx Dosyayı Görüntüle

} 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' })}

server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap → server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap Dosyayı Görüntüle


+ 3
- 3
server/sonar-web/src/main/js/apps/issues/routes.tsx Dosyayı Görüntüle

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 />;
} }

+ 1
- 6
server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx Dosyayı Görüntüle

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;

+ 2
- 4
server/sonar-web/src/main/js/apps/overview/components/App.tsx Dosyayı Görüntüle

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;

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx Dosyayı Görüntüle

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={{

+ 2
- 2
server/sonar-web/src/main/js/apps/projectDeletion/__tests__/App-test.tsx Dosyayı Görüntüle

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 />

+ 0
- 22
server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx Dosyayı Görüntüle

/*
* 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'));

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx Dosyayı Görüntüle

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);

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx Dosyayı Görüntüle

* 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} />;
} }

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx Dosyayı Görüntüle

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>;
} }
); );

+ 2
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx Dosyayı Görüntüle

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, {});
} }

+ 2
- 2
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx Dosyayı Görüntüle

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 });
} }

+ 1
- 6
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx Dosyayı Görüntüle

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>;

+ 647
- 6
server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx Dosyayı Görüntüle

* 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>
);
}
}

+ 0
- 673
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx Dosyayı Görüntüle

/*
* 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>
);
}
}

+ 81
- 3
server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx Dosyayı Görüntüle

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}

server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerBase-test.tsx → server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx Dosyayı Görüntüle

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} />
); );
} }

server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerBase-test.tsx.snap → server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap Dosyayı Görüntüle


+ 0
- 36
server/sonar-web/src/main/js/components/__tests__/__snapshots__/lazyLoadComponent-test.tsx.snap Dosyayı Görüntüle

// 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>
`;

+ 0
- 65
server/sonar-web/src/main/js/components/__tests__/lazyLoadComponent-test.tsx Dosyayı Görüntüle

/*
* 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();
});

+ 1
- 4
server/sonar-web/src/main/js/components/controls/DateInput.tsx Dosyayı Görüntüle

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;

+ 65
- 0
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap Dosyayı Görüntüle

</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",

+ 1
- 3
server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx Dosyayı Görüntüle

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;

+ 0
- 73
server/sonar-web/src/main/js/components/lazyLoadComponent.tsx Dosyayı Görüntüle

/*
* 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;
}
}

+ 1
- 6
server/sonar-web/src/main/js/components/ui/CoverageRating.tsx Dosyayı Görüntüle

*/ */
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 };



+ 3
- 11
server/sonar-web/src/main/js/components/workspace/Workspace.tsx Dosyayı Görüntüle

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>;

+ 1
- 1
server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap Dosyayı Görüntüle

} }
rules={Array []} rules={Array []}
/> />
<WorkspaceComponentViewer
<withBranchStatusActions(WorkspaceComponentViewer)
component={ component={
Object { Object {
"branchLike": Object { "branchLike": Object {

+ 4
- 0
server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap Dosyayı Görüntüle

> >
<SourceViewer <SourceViewer
component="foo" component="foo"
displayAllIssues={false}
displayIssueLocationsCount={true}
displayIssueLocationsLink={true}
displayLocationMarkers={true}
onIssueChange={[Function]} onIssueChange={[Function]}
onLoaded={[Function]} onLoaded={[Function]}
/> />

+ 0
- 15
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts Dosyayı Görüntüle

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', () => {

+ 0
- 2
server/sonar-web/src/main/js/helpers/browser.ts Dosyayı Görüntüle

*/ */
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;
} }

+ 32
- 3
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx Dosyayı Görüntüle

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

+ 0
- 4
server/sonar-web/src/main/js/helpers/urls.ts Dosyayı Görüntüle

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();
} }



Loading…
İptal
Kaydet