Browse Source

SONAR-9736 Build UI for long-living branches (#2390)

tags/6.6-RC1
Stas Vilchik 6 years ago
parent
commit
404d315b07
96 changed files with 2095 additions and 754 deletions
  1. 0
    7
      server/sonar-web/src/main/js/api/branches.ts
  2. 4
    4
      server/sonar-web/src/main/js/api/components.ts
  3. 6
    2
      server/sonar-web/src/main/js/api/measures.ts
  4. 1
    0
      server/sonar-web/src/main/js/api/projectActivity.ts
  5. 34
    40
      server/sonar-web/src/main/js/app/components/ProjectContainer.tsx
  6. 56
    0
      server/sonar-web/src/main/js/app/components/ProjectContainerNotFound.tsx
  7. 63
    2
      server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx
  8. 2
    0
      server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css
  9. 44
    35
      server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx
  10. 9
    4
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  11. 19
    6
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
  12. 51
    77
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
  13. 5
    6
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
  14. 18
    7
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  15. 2
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
  16. 38
    19
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx
  17. 12
    5
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
  18. 29
    24
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
  19. 24
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx
  20. 38
    7
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
  21. 32
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
  22. 19
    3
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap
  23. 28
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
  24. 185
    128
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
  25. 85
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
  26. 144
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  27. 41
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
  28. 8
    0
      server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap
  29. 1
    0
      server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap
  30. 29
    10
      server/sonar-web/src/main/js/apps/code/bucket.ts
  31. 65
    37
      server/sonar-web/src/main/js/apps/code/components/App.tsx
  32. 14
    6
      server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx
  33. 29
    11
      server/sonar-web/src/main/js/apps/code/components/Component.tsx
  34. 8
    2
      server/sonar-web/src/main/js/apps/code/components/ComponentDetach.tsx
  35. 14
    6
      server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
  36. 20
    9
      server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
  37. 11
    7
      server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx
  38. 19
    7
      server/sonar-web/src/main/js/apps/code/components/Components.tsx
  39. 3
    3
      server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx
  40. 10
    6
      server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx
  41. 55
    49
      server/sonar-web/src/main/js/apps/code/components/Search.tsx
  42. 7
    2
      server/sonar-web/src/main/js/apps/code/components/Truncated.tsx
  43. 22
    8
      server/sonar-web/src/main/js/apps/code/types.ts
  44. 56
    39
      server/sonar-web/src/main/js/apps/code/utils.ts
  45. 11
    4
      server/sonar-web/src/main/js/apps/component-measures/components/App.js
  46. 9
    5
      server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js
  47. 4
    2
      server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js
  48. 21
    5
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
  49. 7
    3
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js
  50. 8
    4
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js
  51. 5
    2
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js
  52. 3
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js
  53. 1
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap
  54. 6
    4
      server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js
  55. 3
    1
      server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js
  56. 3
    2
      server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js
  57. 2
    0
      server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js
  58. 4
    2
      server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js
  59. 9
    7
      server/sonar-web/src/main/js/apps/issues/components/App.js
  60. 2
    2
      server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
  61. 8
    2
      server/sonar-web/src/main/js/apps/overview/components/App.js
  62. 21
    14
      server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
  63. 13
    2
      server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
  64. 6
    1
      server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js
  65. 5
    3
      server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js
  66. 8
    2
      server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js
  67. 10
    4
      server/sonar-web/src/main/js/apps/overview/main/Coverage.js
  68. 13
    4
      server/sonar-web/src/main/js/apps/overview/main/Duplications.js
  69. 17
    7
      server/sonar-web/src/main/js/apps/overview/main/enhance.js
  70. 3
    1
      server/sonar-web/src/main/js/apps/overview/meta/Meta.js
  71. 1
    1
      server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js
  72. 10
    2
      server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js
  73. 3
    2
      server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js
  74. 7
    3
      server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js
  75. 10
    3
      server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js
  76. 74
    9
      server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js
  77. 1
    0
      server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap
  78. 67
    0
      server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
  79. 8
    3
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
  80. 1
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js
  81. 22
    7
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
  82. 17
    6
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
  83. 1
    1
      server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js
  84. 1
    1
      server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js
  85. 7
    4
      server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js
  86. 7
    2
      server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
  87. 10
    17
      server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx
  88. 45
    0
      server/sonar-web/src/main/js/components/icons-components/LongLivingBranchIcon.tsx
  89. 43
    0
      server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx
  90. 45
    0
      server/sonar-web/src/main/js/components/icons-components/ShortLivingBranchIcon.tsx
  91. 10
    2
      server/sonar-web/src/main/js/components/shared/drilldown-link.js
  92. 71
    0
      server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts
  93. 46
    11
      server/sonar-web/src/main/js/helpers/branches.ts
  94. 6
    4
      server/sonar-web/src/main/js/helpers/testUtils.ts
  95. 18
    9
      server/sonar-web/src/main/js/helpers/urls.ts
  96. 2
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 0
- 7
server/sonar-web/src/main/js/api/branches.ts View File

@@ -23,10 +23,3 @@ import throwGlobalError from '../app/utils/throwGlobalError';
export function getBranches(project: string): Promise<any> {
return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError);
}

export function getBranch(project: string, branch: string): Promise<any> {
return getJSON('/api/project_branches/show', { component: project, branch }).then(
r => r.branch,
throwGlobalError
);
}

+ 4
- 4
server/sonar-web/src/main/js/api/components.ts View File

@@ -243,11 +243,11 @@ export function getSources(
return getJSON('/api/sources/lines', data).then(r => r.sources);
}

export function getDuplications(component: string): Promise<any> {
return getJSON('/api/duplications/show', { key: component });
export function getDuplications(component: string, branch?: string): Promise<any> {
return getJSON('/api/duplications/show', { key: component, branch });
}

export function getTests(component: string, line: number | string): Promise<any> {
const data = { sourceFileKey: component, sourceFileLineNumber: line };
export function getTests(component: string, line: number | string, branch?: string): Promise<any> {
const data = { sourceFileKey: component, sourceFileLineNumber: line, branch };
return getJSON('/api/tests/list', data).then(r => r.tests);
}

+ 6
- 2
server/sonar-web/src/main/js/api/measures.ts View File

@@ -19,9 +19,13 @@
*/
import { getJSON, RequestData } from '../helpers/request';

export function getMeasures(componentKey: string, metrics: string[]): Promise<any> {
export function getMeasures(
componentKey: string,
metrics: string[],
branch?: string
): Promise<any> {
const url = '/api/measures/component';
const data = { componentKey, metricKeys: metrics.join(',') };
const data = { componentKey, metricKeys: metrics.join(','), branch };
return getJSON(url, data).then(r => r.component.measures);
}


+ 1
- 0
server/sonar-web/src/main/js/api/projectActivity.ts View File

@@ -30,6 +30,7 @@ interface GetProjectActivityResponse {
}

export function getProjectActivity(data: {
branch?: string;
project: string;
category?: string;
p?: number;

+ 34
- 40
server/sonar-web/src/main/js/app/components/ProjectContainer.tsx View File

@@ -18,13 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import ProjectContainerNotFound from './ProjectContainerNotFound';
import ComponentNav from './nav/component/ComponentNav';
import { Branch, Component } from '../types';
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
import { getBranch } from '../../api/branches';
import { getBranches } from '../../api/branches';
import { getComponentData } from '../../api/components';
import { getComponentNavigation } from '../../api/nav';
import { MAIN_BRANCH } from '../../helpers/branches';

interface Props {
children: any;
@@ -34,7 +34,7 @@ interface Props {
}

interface State {
branch: Branch | null;
branches: Branch[];
loading: boolean;
component: Component | null;
}
@@ -44,7 +44,7 @@ export default class ProjectContainer extends React.PureComponent<Props, State>

constructor(props: Props) {
super(props);
this.state = { branch: null, loading: true, component: null };
this.state = { branches: [], loading: true, component: null };
}

componentDidMount() {
@@ -52,19 +52,8 @@ export default class ProjectContainer extends React.PureComponent<Props, State>
this.fetchProject();
}

componentWillReceiveProps(nextProps: Props) {
// if the current branch has been changed, reset `branch` in state
// it prevents unwanted redirect in `overview/App#componentDidMount`
if (nextProps.location.query.branch !== this.props.location.query.branch) {
this.setState({ branch: null });
}
}

componentDidUpdate(prevProps: Props) {
if (
prevProps.location.query.id !== this.props.location.query.id ||
prevProps.location.query.branch !== this.props.location.query.branch
) {
if (prevProps.location.query.id !== this.props.location.query.id) {
this.fetchProject();
}
}
@@ -81,30 +70,27 @@ export default class ProjectContainer extends React.PureComponent<Props, State>
fetchProject() {
const { branch, id } = this.props.location.query;
this.setState({ loading: true });
Promise.all([
getComponentNavigation(id),
getComponentData(id, branch),
branch && getBranch(id, branch)
]).then(
([nav, data, branch]) => {
if (this.mounted) {
this.setState({
loading: false,
branch: branch || MAIN_BRANCH,
component: this.addQualifier({ ...nav, ...data })
});

const onError = (error: any) => {
if (this.mounted) {
if (error.response && error.response.status === 403) {
handleRequiredAuthorization();
} else {
this.setState({ loading: false });
}
},
error => {
}
};

Promise.all([getComponentNavigation(id), getComponentData(id, branch)]).then(([nav, data]) => {
const component = this.addQualifier({ ...nav, ...data });
const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK');
const branchesRequest = project ? getBranches(project.key) : Promise.resolve([]);
branchesRequest.then(branches => {
if (this.mounted) {
if (error.response && error.response.status === 403) {
handleRequiredAuthorization();
} else {
this.setState({ loading: false });
}
this.setState({ loading: false, branches, component });
}
}
);
}, onError);
}, onError);
}

handleProjectChange = (changes: {}) => {
@@ -114,10 +100,17 @@ export default class ProjectContainer extends React.PureComponent<Props, State>
};

render() {
const { branch, component } = this.state;
const { query } = this.props.location;
const { branches, component, loading } = this.state;

if (loading) {
return <i className="spinner" />;
}

const branch = branches.find(b => (query.branch ? b.name === query.branch : b.isMain));

if (!component || !branch) {
return null;
return <ProjectContainerNotFound />;
}

const isFile = ['FIL', 'UTS'].includes(component.qualifier);
@@ -127,7 +120,8 @@ export default class ProjectContainer extends React.PureComponent<Props, State>
<div>
{!isFile &&
<ComponentNav
branch={branch}
branches={branches}
currentBranch={branch}
component={component}
conf={configuration}
location={this.props.location}

+ 56
- 0
server/sonar-web/src/main/js/app/components/ProjectContainerNotFound.tsx View File

@@ -0,0 +1,56 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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 { Link } from 'react-router';
import { translate } from '../../helpers/l10n';

export default class ProjectContainerNotFound extends React.PureComponent {
componentDidMount() {
const html = document.querySelector('html');
if (html) {
html.classList.add('dashboard-page');
}
}

componentWillUnmount() {
const html = document.querySelector('html');
if (html) {
html.classList.remove('dashboard-page');
}
}

render() {
return (
<div id="bd" className="page-wrapper-simple">
<div id="nonav" className="page-simple">
<h2 className="big-spacer-bottom">
{translate('dashboard.project_not_found')}
</h2>
<p className="spacer-bottom">
{translate('dashboard.project_not_found.2')}
</p>
<p>
<Link to="/">Go back to the homepage</Link>
</p>
</div>
</div>
);
}
}

+ 63
- 2
server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx View File

@@ -17,9 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
jest.mock('../../../api/branches', () => ({ getBranches: jest.fn() }));
jest.mock('../../../api/components', () => ({ getComponentData: jest.fn() }));
jest.mock('../../../api/nav', () => ({ getComponentNavigation: jest.fn() }));

import * as React from 'react';
import { shallow } from 'enzyme';
import { shallow, mount } from 'enzyme';
import ProjectContainer from '../ProjectContainer';
import { getBranches } from '../../../api/branches';
import { getComponentData } from '../../../api/components';
import { getComponentNavigation } from '../../../api/nav';
import { doAsync } from '../../../helpers/testUtils';

beforeEach(() => {
(getBranches as jest.Mock<any>).mockClear();
(getComponentData as jest.Mock<any>).mockClear();
(getComponentNavigation as jest.Mock<any>).mockClear();
});

it('changes component', () => {
const Inner = () => <div />;
@@ -31,7 +45,7 @@ it('changes component', () => {
);
(wrapper.instance() as ProjectContainer).mounted = true;
wrapper.setState({
branch: { isMain: true },
branches: [{ isMain: true }],
component: { qualifier: 'TRK', visibility: 'public' },
loading: false
});
@@ -39,3 +53,50 @@ it('changes component', () => {
(wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: 'private' });
expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' });
});

it("loads branches for module's project", () => {
(getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
(getComponentData as jest.Mock<any>).mockImplementation(() => Promise.resolve({}));
(getComponentNavigation as jest.Mock<any>).mockImplementation(() =>
Promise.resolve({
breadcrumbs: [
{ key: 'projectKey', name: 'project', qualifier: 'TRK' },
{ key: 'moduleKey', name: 'module', qualifier: 'BRC' }
]
})
);

mount(
<ProjectContainer location={{ query: { id: 'moduleKey' } }}>
<div />
</ProjectContainer>
);

return doAsync().then(() => {
expect(getBranches).toBeCalledWith('projectKey');
expect(getComponentData).toBeCalledWith('moduleKey', undefined);
expect(getComponentNavigation).toBeCalledWith('moduleKey');
});
});

it("doesn't load branches portfolio", () => {
(getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
(getComponentData as jest.Mock<any>).mockImplementation(() => Promise.resolve({}));
(getComponentNavigation as jest.Mock<any>).mockImplementation(() =>
Promise.resolve({
breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }]
})
);

mount(
<ProjectContainer location={{ query: { id: 'portfolioKey' } }}>
<div />
</ProjectContainer>
);

return doAsync().then(() => {
expect(getBranches).not.toBeCalled();
expect(getComponentData).toBeCalledWith('portfolioKey', undefined);
expect(getComponentNavigation).toBeCalledWith('portfolioKey');
});
});

+ 2
- 0
server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css View File

@@ -1,4 +1,6 @@
.branch-status {
min-width: 64px;
text-align: right;
}

.branch-status-indicator {

+ 44
- 35
server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx View File

@@ -20,6 +20,7 @@
import * as React from 'react';
import * as classNames from 'classnames';
import { Branch } from '../../../types';
import Level from '../../../../components/ui/Level';
import BugIcon from '../../../../components/icons-components/BugIcon';
import CodeSmellIcon from '../../../../components/icons-components/CodeSmellIcon';
import VulnerabilityIcon from '../../../../components/icons-components/VulnerabilityIcon';
@@ -32,42 +33,50 @@ interface Props {
}

export default function BranchStatus({ branch, concise = false }: Props) {
// TODO handle long-living branches
if (!isShortLivingBranch(branch)) {
return null;
}
if (isShortLivingBranch(branch)) {
if (!branch.status) {
return null;
}

const totalIssues = branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells;
const totalIssues =
branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells;

return (
<ul className="list-inline branch-status">
<li>
<i
className={classNames('branch-status-indicator', {
'is-failed': totalIssues > 0,
'is-passed': totalIssues === 0
})}
/>
</li>
{concise &&
<li>
{totalIssues}
</li>}
{!concise &&
<li>
{branch.status.bugs}
<BugIcon className="little-spacer-left" />
</li>}
{!concise &&
return (
<ul className="list-inline branch-status">
<li>
{branch.status.vulnerabilities}
<VulnerabilityIcon className="little-spacer-left" />
</li>}
{!concise &&
<li>
{branch.status.codeSmells}
<CodeSmellIcon className="little-spacer-left" />
</li>}
</ul>
);
<i
className={classNames('branch-status-indicator', {
'is-failed': totalIssues > 0,
'is-passed': totalIssues === 0
})}
/>
</li>
{concise &&
<li>
{totalIssues}
</li>}
{!concise &&
<li>
{branch.status.bugs}
<BugIcon className="little-spacer-left" />
</li>}
{!concise &&
<li>
{branch.status.vulnerabilities}
<VulnerabilityIcon className="little-spacer-left" />
</li>}
{!concise &&
<li>
{branch.status.codeSmells}
<CodeSmellIcon className="little-spacer-left" />
</li>}
</ul>
);
} else {
if (!branch.status) {
return null;
}

return <Level level={branch.status.qualityGateStatus} small={true} />;
}
}

+ 9
- 4
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx View File

@@ -31,7 +31,8 @@ import { STATUSES } from '../../../../apps/background-tasks/constants';
import './ComponentNav.css';

interface Props {
branch: Branch;
branches: Branch[];
currentBranch: Branch;
component: Component;
conf: ComponentConfiguration;
location: {};
@@ -98,17 +99,21 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
breadcrumbs={this.props.component.breadcrumbs}
/>

<ComponentNavBranch branch={this.props.branch} project={this.props.component} />
<ComponentNavBranch
branches={this.props.branches}
currentBranch={this.props.currentBranch}
project={this.props.component}
/>

<ComponentNavMeta
branch={this.props.branch}
branch={this.props.currentBranch}
component={this.props.component}
conf={this.props.conf}
incremental={this.state.incremental}
/>

<ComponentNavMenu
branch={this.props.branch}
branch={this.props.currentBranch}
component={this.props.component}
conf={this.props.conf}
/>

+ 19
- 6
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx View File

@@ -22,10 +22,12 @@ import * as classNames from 'classnames';
import ComponentNavBranchesMenu from './ComponentNavBranchesMenu';
import { Branch, Component } from '../../../types';
import BranchIcon from '../../../../components/icons-components/BranchIcon';
import { getBranchDisplayName } from '../../../../helpers/branches';
import { isShortLivingBranch } from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';

interface Props {
branch: Branch;
branches: Branch[];
currentBranch: Branch;
project: Component;
}

@@ -42,7 +44,10 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
}

componentWillReceiveProps(nextProps: Props) {
if (nextProps.project !== this.props.project || nextProps.branch !== this.props.branch) {
if (
nextProps.project !== this.props.project ||
nextProps.currentBranch !== this.props.currentBranch
) {
this.setState({ open: false });
}
}
@@ -65,19 +70,27 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
};

render() {
const { currentBranch } = this.props;

return (
<div className={classNames('navbar-context-branches', 'dropdown', { open: this.state.open })}>
<a className="link-base-color link-no-underline" href="#" onClick={this.handleClick}>
<BranchIcon className="little-spacer-right" />
{getBranchDisplayName(this.props.branch)}
<BranchIcon branch={currentBranch} className="little-spacer-right" />
{currentBranch.name}
<i className="icon-dropdown little-spacer-left" />
</a>
{this.state.open &&
<ComponentNavBranchesMenu
branch={this.props.branch}
branches={this.props.branches}
currentBranch={currentBranch}
onClose={this.closeDropdown}
project={this.props.project}
/>}
{isShortLivingBranch(currentBranch) &&
!currentBranch.isOrphan &&
<span className="note big-spacer-left text-lowercase">
{translate('from')} <strong>{currentBranch.mergeBranch}</strong>
</span>}
</div>
);
}

+ 51
- 77
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx View File

@@ -19,83 +19,47 @@
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { sortBy } from 'lodash';
import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem';
import { Branch, Component } from '../../../types';
import { getBranches } from '../../../../api/branches';
import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches';
import {
sortBranchesAsTree,
isLongLivingBranch,
isShortLivingBranch
} from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
import { getProjectBranchUrl } from '../../../../helpers/urls';

interface Props {
branch: Branch;
branches: Branch[];
currentBranch: Branch;
onClose: () => void;
project: Component;
}

interface State {
branches: Branch[];
loading: boolean;
query: string;
selected: string | null;
}

export default class ComponentNavBranchesMenu extends React.PureComponent<Props, State> {
private mounted: boolean;
private node: HTMLElement | null;
state = { query: '', selected: null };

static contextTypes = {
router: PropTypes.object
};

constructor(props: Props) {
super(props);
this.state = {
branches: [],
loading: true,
query: '',
selected: null
};
}

componentDidMount() {
this.mounted = true;
this.fetchBranches();
window.addEventListener('click', this.handleClickOutside);
}

componentWillUnmount() {
this.mounted = false;
window.removeEventListener('click', this.handleClickOutside);
}

fetchBranches = () => {
this.setState({ loading: true });
getBranches(this.props.project.key).then(
(branches: Branch[]) => {
if (this.mounted) {
this.setState({ branches: this.sortBranches(branches), loading: false });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

sortBranches = (branches: Branch[]): Branch[] =>
sortBy(
branches,
branch => !branch.isMain, // main branch first
branch => !isShortLivingBranch(branch), // then short-living branches
branch => getBranchDisplayName(branch) // then by name
);

getFilteredBranches = () =>
this.state.branches.filter(branch =>
getBranchDisplayName(branch).toLowerCase().includes(this.state.query.toLowerCase())
sortBranchesAsTree(this.props.branches).filter(branch =>
branch.name.toLowerCase().includes(this.state.query.toLowerCase())
);

handleClickOutside = (event: Event) => {
@@ -130,9 +94,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,

openSelected = () => {
const selected = this.getSelected();
const branch = this.getFilteredBranches().find(
branch => getBranchDisplayName(branch) === selected
);
const branch = this.getFilteredBranches().find(branch => branch.name === selected);
if (branch) {
this.context.router.push(this.getProjectBranchUrl(branch));
}
@@ -141,33 +103,33 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
selectPrevious = () => {
const selected = this.getSelected();
const branches = this.getFilteredBranches();
const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected);
const index = branches.findIndex(branch => branch.name === selected);
if (index > 0) {
this.setState({ selected: getBranchDisplayName(branches[index - 1]) });
this.setState({ selected: branches[index - 1].name });
}
};

selectNext = () => {
const selected = this.getSelected();
const branches = this.getFilteredBranches();
const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected);
const index = branches.findIndex(branch => branch.name === selected);
if (index >= 0 && index < branches.length - 1) {
this.setState({ selected: getBranchDisplayName(branches[index + 1]) });
this.setState({ selected: branches[index + 1].name });
}
};

handleSelect = (branch: Branch) => {
this.setState({ selected: getBranchDisplayName(branch) });
this.setState({ selected: branch.name });
};

getSelected = () => {
const branches = this.getFilteredBranches();
return this.state.selected || (branches.length > 0 && getBranchDisplayName(branches[0]));
return this.state.selected || (branches.length > 0 && branches[0].name);
};

getProjectBranchUrl = (branch: Branch) => getProjectBranchUrl(this.props.project.key, branch);

isSelected = (branch: Branch) => getBranchDisplayName(branch) === this.getSelected();
isSelected = (branch: Branch) => branch.name === this.getSelected();

renderSearch = () =>
<div className="search-box menu-search">
@@ -187,35 +149,47 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,

renderBranchesList = () => {
const branches = this.getFilteredBranches();

const selected = this.getSelected();

return branches.length > 0
? <ul className="menu">
{branches.map(branch =>
<ComponentNavBranchesMenuItem
branch={branch}
component={this.props.project}
key={getBranchDisplayName(branch)}
onSelect={this.handleSelect}
selected={getBranchDisplayName(branch) === selected}
/>
)}
</ul>
: <div className="menu-message note">
if (branches.length === 0) {
return (
<div className="menu-message note">
{translate('no_results')}
</div>;
</div>
);
}

const menu: JSX.Element[] = [];
branches.forEach((branch, index) => {
const isOrphan = isShortLivingBranch(branch) && branch.isOrphan;
const previous = index > 0 ? branches[index - 1] : null;
const isPreviousOrphan = isShortLivingBranch(previous) ? previous.isOrphan : false;
if (isLongLivingBranch(branch) || (isOrphan && !isPreviousOrphan)) {
menu.push(<li key={`divider-${branch.name}`} className="divider" />);
}
menu.push(
<ComponentNavBranchesMenuItem
branch={branch}
component={this.props.project}
key={branch.name}
onSelect={this.handleSelect}
selected={branch.name === selected}
/>
);
});

return (
<ul className="menu">
{menu}
</ul>
);
};

render() {
return (
<div className="dropdown-menu dropdown-menu-shadow" ref={node => (this.node = node)}>
{this.state.loading
? <i className="spinner" />
: <div>
{this.renderSearch()}
{this.renderBranchesList()}
</div>}
{this.renderSearch()}
{this.renderBranchesList()}
</div>
);
}

+ 5
- 6
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx View File

@@ -23,7 +23,7 @@ import * as classNames from 'classnames';
import BranchStatus from './BranchStatus';
import { Branch, Component } from '../../../types';
import BranchIcon from '../../../../components/icons-components/BranchIcon';
import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches';
import { isShortLivingBranch } from '../../../../helpers/branches';
import { getProjectBranchUrl } from '../../../../helpers/urls';

interface Props {
@@ -34,14 +34,12 @@ interface Props {
}

export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) {
const displayName = getBranchDisplayName(branch);

const handleMouseEnter = () => {
props.onSelect(branch);
};

return (
<li key={displayName} onMouseEnter={handleMouseEnter}>
<li key={branch.name} onMouseEnter={handleMouseEnter}>
<Link
className={classNames('navbar-context-meta-branch-menu-item', {
active: props.selected
@@ -49,11 +47,12 @@ export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props
to={getProjectBranchUrl(props.component.key, branch)}>
<div>
<BranchIcon
branch={branch}
className={classNames('little-spacer-right', {
'big-spacer-left': isShortLivingBranch(branch)
'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
})}
/>
{displayName}
{branch.name}
</div>
<div className="big-spacer-left note">
<BranchStatus branch={branch} concise={true} />

+ 18
- 7
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx View File

@@ -22,7 +22,7 @@ import { Link } from 'react-router';
import * as classNames from 'classnames';
import { Branch, Component, ComponentExtension, ComponentConfiguration } from '../../../types';
import NavBarTabs from '../../../../components/nav/NavBarTabs';
import { isShortLivingBranch } from '../../../../helpers/branches';
import { isShortLivingBranch, getBranchName } from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';

const SETTINGS_URLS = [
@@ -71,7 +71,12 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
const pathname = this.isView() ? '/portfolio' : '/dashboard';
return (
<li>
<Link to={{ pathname, query: { id: this.props.component.key } }} activeClassName="active">
<Link
to={{
pathname,
query: { branch: getBranchName(this.props.branch), id: this.props.component.key }
}}
activeClassName="active">
{translate('overview.page')}
</Link>
</li>
@@ -88,7 +93,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
<Link
to={{
pathname: '/code',
query: { branch: this.props.branch.name, id: this.props.component.key }
query: { branch: getBranchName(this.props.branch), id: this.props.component.key }
}}
activeClassName="active">
{this.isView() || this.isApplication()
@@ -111,7 +116,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
return (
<li>
<Link
to={{ pathname: '/project/activity', query: { id: this.props.component.key } }}
to={{
pathname: '/project/activity',
query: { branch: getBranchName(this.props.branch), id: this.props.component.key }
}}
activeClassName="active">
{translate('project_activity.page')}
</Link>
@@ -126,7 +134,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
to={{
pathname: '/project/issues',
query: {
branch: this.props.branch.name,
branch: getBranchName(this.props.branch),
id: this.props.component.key,
resolved: 'false'
}
@@ -146,7 +154,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
return (
<li>
<Link
to={{ pathname: '/component_measures', query: { id: this.props.component.key } }}
to={{
pathname: '/component_measures',
query: { branch: getBranchName(this.props.branch), id: this.props.component.key }
}}
activeClassName="active">
{translate('layout.measures')}
</Link>
@@ -155,7 +166,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
}

renderAdministration() {
if (isShortLivingBranch(this.props.branch)) {
if (!this.props.branch.isMain) {
return null;
}


+ 2
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx View File

@@ -25,6 +25,7 @@ import Tooltip from '../../../../components/controls/Tooltip';
import PendingIcon from '../../../../components/icons-components/PendingIcon';
import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { isShortLivingBranch } from '../../../../helpers/branches';

interface Props {
branch: Branch;
@@ -114,7 +115,7 @@ export default function ComponentNavMeta(props: Props) {
);
}

if (!props.branch.isMain) {
if (isShortLivingBranch(props.branch)) {
metaList.push(
<li className="navbar-context-meta-branch" key="branch-status">
<BranchStatus branch={props.branch} />

+ 38
- 19
server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx View File

@@ -20,25 +20,44 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import BranchStatus from '../BranchStatus';
import { BranchType } from '../../../../types';
import { BranchType, LongLivingBranch } from '../../../../types';

it('renders', () => {
check(0, 0, 0);
check(0, 1, 0);
check(7, 3, 6);
it('renders status of short-living branches', () => {
checkShort(0, 0, 0);
checkShort(0, 1, 0);
checkShort(7, 3, 6);

function checkShort(bugs: number, codeSmells: number, vulnerabilities: number) {
expect(
shallow(
<BranchStatus
branch={{
isMain: false,
mergeBranch: 'master',
name: 'foo',
status: { bugs, codeSmells, vulnerabilities },
type: BranchType.SHORT
}}
/>
)
).toMatchSnapshot();
}
});

function check(bugs: number, codeSmells: number, vulnerabilities: number) {
expect(
shallow(
<BranchStatus
branch={{
isMain: false,
name: 'foo',
status: { bugs, codeSmells, vulnerabilities },
type: BranchType.SHORT
}}
/>
)
).toMatchSnapshot();
}
it('renders status of long-living branches', () => {
checkLong();
checkLong('OK');
checkLong('ERROR');

function checkLong(qualityGateStatus?: string) {
const branch: LongLivingBranch = {
isMain: false,
name: 'foo',
type: BranchType.LONG
};
if (qualityGateStatus) {
branch.status = { qualityGateStatus };
}
expect(shallow(<BranchStatus branch={branch} />)).toMatchSnapshot();
}
});

+ 12
- 5
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx View File

@@ -24,26 +24,33 @@ import { BranchType, ShortLivingBranch, MainBranch, Component } from '../../../.
import { click } from '../../../../../helpers/testUtils';

it('renders main branch', () => {
const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG };
const branch: MainBranch = { isMain: true, name: 'master' };
const component = {} as Component;
expect(shallow(<ComponentNavBranch branch={branch} project={component} />)).toMatchSnapshot();
expect(
shallow(<ComponentNavBranch branches={[]} currentBranch={branch} project={component} />)
).toMatchSnapshot();
});

it('renders short-living branch', () => {
const branch: ShortLivingBranch = {
isMain: false,
mergeBranch: 'master',
name: 'foo',
status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 },
type: BranchType.SHORT
};
const component = {} as Component;
expect(shallow(<ComponentNavBranch branch={branch} project={component} />)).toMatchSnapshot();
expect(
shallow(<ComponentNavBranch branches={[]} currentBranch={branch} project={component} />)
).toMatchSnapshot();
});

it('opens menu', () => {
const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG };
const branch: MainBranch = { isMain: true, name: 'master' };
const component = {} as Component;
const wrapper = shallow(<ComponentNavBranch branch={branch} project={component} />);
const wrapper = shallow(
<ComponentNavBranch branches={[]} currentBranch={branch} project={component} />
);
expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0);
click(wrapper.find('a'));
expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1);

+ 29
- 24
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx View File

@@ -29,40 +29,43 @@ import {
} from '../../../../types';
import { elementKeydown } from '../../../../../helpers/testUtils';

const project = { key: 'component' } as Component;

it('renders list', () => {
const component = { key: 'component' } as Component;
const wrapper = shallow(
<ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} />
);
wrapper.setState({
branches: [mainBranch(), shortBranch('foo'), longBranch('bar')],
loading: false
});
expect(wrapper).toMatchSnapshot();
expect(
shallow(
<ComponentNavBranchesMenu
branches={[mainBranch(), shortBranch('foo'), longBranch('bar'), shortBranch('baz', true)]}
currentBranch={mainBranch()}
onClose={jest.fn()}
project={project}
/>
)
).toMatchSnapshot();
});

it('searches', () => {
const component = { key: 'component' } as Component;
const wrapper = shallow(
<ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} />
<ComponentNavBranchesMenu
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
currentBranch={mainBranch()}
onClose={jest.fn()}
project={project}
/>
);
wrapper.setState({
branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')],
loading: false,
query: 'bar'
});
wrapper.setState({ query: 'bar' });
expect(wrapper).toMatchSnapshot();
});

it('selects next & previous', () => {
const component = { key: 'component' } as Component;
const wrapper = shallow(
<ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} />
<ComponentNavBranchesMenu
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
currentBranch={mainBranch()}
onClose={jest.fn()}
project={project}
/>
);
wrapper.setState({
branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')],
loading: false
});
elementKeydown(wrapper.find('input'), 40);
wrapper.update();
expect(wrapper.state().selected).toBe('foo');
@@ -75,12 +78,14 @@ it('selects next & previous', () => {
});

function mainBranch(): MainBranch {
return { isMain: true, name: undefined, type: BranchType.LONG };
return { isMain: true, name: 'master' };
}

function shortBranch(name: string): ShortLivingBranch {
function shortBranch(name: string, isOrphan?: true): ShortLivingBranch {
return {
isMain: false,
isOrphan,
mergeBranch: 'master',
name,
status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 },
type: BranchType.SHORT

+ 24
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx View File

@@ -24,7 +24,7 @@ import { BranchType, MainBranch, ShortLivingBranch, Component } from '../../../.

it('renders main branch', () => {
const component = { key: 'component' } as Component;
const mainBranch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG };
const mainBranch: MainBranch = { isMain: true, name: 'master' };
expect(
shallow(
<ComponentNavBranchesMenuItem
@@ -41,6 +41,29 @@ it('renders short-living branch', () => {
const component = { key: 'component' } as Component;
const shortBranch: ShortLivingBranch = {
isMain: false,
mergeBranch: 'master',
name: 'foo',
status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 },
type: BranchType.SHORT
};
expect(
shallow(
<ComponentNavBranchesMenuItem
branch={shortBranch}
component={component}
onSelect={jest.fn()}
selected={false}
/>
)
).toMatchSnapshot();
});

it('renders short-living orhpan branch', () => {
const component = { key: 'component' } as Component;
const shortBranch: ShortLivingBranch = {
isMain: false,
isOrphan: true,
mergeBranch: 'master',
name: 'foo',
status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 },
type: BranchType.SHORT

+ 38
- 7
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx View File

@@ -20,7 +20,18 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavMenu from '../ComponentNavMenu';
import { Branch, Component } from '../../../../types';
import {
Component,
ShortLivingBranch,
BranchType,
LongLivingBranch,
MainBranch
} from '../../../../types';

const mainBranch: MainBranch = {
isMain: true,
name: 'master'
};

it('should work with extensions', () => {
const component = {
@@ -33,9 +44,7 @@ it('should work with extensions', () => {
extensions: [{ key: 'foo', name: 'Foo' }]
};
expect(
shallow(
<ComponentNavMenu branch={{} as Branch} component={component as Component} conf={conf} />
)
shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />)
).toMatchSnapshot();
});

@@ -53,8 +62,30 @@ it('should work with multiple extensions', () => {
extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]
};
expect(
shallow(
<ComponentNavMenu branch={{} as Branch} component={component as Component} conf={conf} />
)
shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />)
).toMatchSnapshot();
});

it('should work for short-living branches', () => {
const branch: ShortLivingBranch = {
isMain: false,
mergeBranch: 'master',
name: 'feature',
status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 },
type: BranchType.SHORT
};
const component = { key: 'foo', qualifier: 'TRK' } as Component;
const conf = { showSettings: true };
expect(
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />)
).toMatchSnapshot();
});

it('should work for long-living branches', () => {
const branch: LongLivingBranch = { isMain: false, name: 'release', type: BranchType.LONG };
const component = { key: 'foo', qualifier: 'TRK' } as Component;
const conf = { showSettings: true };
expect(
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />)
).toMatchSnapshot();
});

+ 32
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx View File

@@ -20,7 +20,13 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavMeta from '../ComponentNavMeta';
import { Branch, Component } from '../../../../types';
import {
Branch,
Component,
BranchType,
ShortLivingBranch,
LongLivingBranch
} from '../../../../types';

it('renders incremental badge', () => {
check(true);
@@ -39,3 +45,28 @@ it('renders incremental badge', () => {
).toHaveLength(incremental ? 1 : 0);
}
});

it('renders status of short-living branch', () => {
const branch: ShortLivingBranch = {
isMain: false,
mergeBranch: 'master',
name: 'feature',
status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 },
type: BranchType.SHORT
};
expect(
shallow(<ComponentNavMeta branch={branch} component={{ key: 'foo' } as Component} conf={{}} />)
).toMatchSnapshot();
});

it('renders nothing for long-living branch', () => {
const branch: LongLivingBranch = {
isMain: false,
name: 'release',
status: { qualityGateStatus: 'OK' },
type: BranchType.LONG
};
expect(
shallow(<ComponentNavMeta branch={branch} component={{ key: 'foo' } as Component} conf={{}} />)
).toMatchSnapshot();
});

+ 19
- 3
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap View File

@@ -1,6 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
exports[`renders status of long-living branches 1`] = `null`;

exports[`renders status of long-living branches 2`] = `
<Level
level="OK"
small={true}
/>
`;

exports[`renders status of long-living branches 3`] = `
<Level
level="ERROR"
small={true}
/>
`;

exports[`renders status of short-living branches 1`] = `
<ul
className="list-inline branch-status"
>
@@ -30,7 +46,7 @@ exports[`renders 1`] = `
</ul>
`;

exports[`renders 2`] = `
exports[`renders status of short-living branches 2`] = `
<ul
className="list-inline branch-status"
>
@@ -60,7 +76,7 @@ exports[`renders 2`] = `
</ul>
`;

exports[`renders 3`] = `
exports[`renders status of short-living branches 3`] = `
<ul
className="list-inline branch-status"
>

+ 28
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap View File

@@ -10,6 +10,12 @@ exports[`renders main branch 1`] = `
onClick={[Function]}
>
<BranchIcon
branch={
Object {
"isMain": true,
"name": "master",
}
}
className="little-spacer-right"
/>
master
@@ -30,6 +36,19 @@ exports[`renders short-living branch 1`] = `
onClick={[Function]}
>
<BranchIcon
branch={
Object {
"isMain": false,
"mergeBranch": "master",
"name": "foo",
"status": Object {
"bugs": 0,
"codeSmells": 0,
"vulnerabilities": 0,
},
"type": "SHORT",
}
}
className="little-spacer-right"
/>
foo
@@ -37,5 +56,14 @@ exports[`renders short-living branch 1`] = `
className="icon-dropdown little-spacer-left"
/>
</a>
<span
className="note big-spacer-left text-lowercase"
>
from

<strong>
master
</strong>
</span>
</div>
`;

+ 185
- 128
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap View File

@@ -4,85 +4,139 @@ exports[`renders list 1`] = `
<div
className="dropdown-menu dropdown-menu-shadow"
>
<div>
<div
className="search-box menu-search"
<div
className="search-box menu-search"
>
<button
className="search-box-submit button-clean"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search-new"
/>
</button>
<input
autoFocus={true}
className="search-box-input"
onChange={[Function]}
onKeyDown={[Function]}
placeholder="search_verb"
type="search"
value=""
<i
className="icon-search-new"
/>
</div>
<ul
className="menu"
>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": true,
"name": undefined,
"type": "LONG",
}
</button>
<input
autoFocus={true}
className="search-box-input"
onChange={[Function]}
onKeyDown={[Function]}
placeholder="search_verb"
type="search"
value=""
/>
</div>
<ul
className="menu"
>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": true,
"name": "master",
}
component={
Object {
"key": "component",
}
}
component={
Object {
"key": "component",
}
onSelect={[Function]}
selected={true}
/>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"name": "foo",
"status": Object {
"bugs": 0,
"codeSmells": 0,
"vulnerabilities": 0,
},
"type": "SHORT",
}
}
onSelect={[Function]}
selected={true}
/>
<li
className="divider"
/>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"isOrphan": true,
"mergeBranch": "master",
"name": "baz",
"status": Object {
"bugs": 0,
"codeSmells": 0,
"vulnerabilities": 0,
},
"type": "SHORT",
}
component={
Object {
"key": "component",
}
}
component={
Object {
"key": "component",
}
onSelect={[Function]}
selected={false}
/>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"name": "bar",
"type": "LONG",
}
}
onSelect={[Function]}
selected={false}
/>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"isOrphan": undefined,
"mergeBranch": "master",
"name": "foo",
"status": Object {
"bugs": 0,
"codeSmells": 0,
"vulnerabilities": 0,
},
"type": "SHORT",
}
component={
Object {
"key": "component",
}
}
component={
Object {
"key": "component",
}
onSelect={[Function]}
selected={false}
/>
</ul>
</div>
}
onSelect={[Function]}
selected={false}
/>
<li
className="divider"
/>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"name": "bar",
"type": "LONG",
}
}
component={
Object {
"key": "component",
}
}
onSelect={[Function]}
selected={false}
/>
<li
className="divider"
/>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"isOrphan": true,
"mergeBranch": "master",
"name": "baz",
"status": Object {
"bugs": 0,
"codeSmells": 0,
"vulnerabilities": 0,
},
"type": "SHORT",
}
}
component={
Object {
"key": "component",
}
}
onSelect={[Function]}
selected={false}
/>
</ul>
</div>
`;

@@ -90,68 +144,71 @@ exports[`searches 1`] = `
<div
className="dropdown-menu dropdown-menu-shadow"
>
<div>
<div
className="search-box menu-search"
<div
className="search-box menu-search"
>
<button
className="search-box-submit button-clean"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search-new"
/>
</button>
<input
autoFocus={true}
className="search-box-input"
onChange={[Function]}
onKeyDown={[Function]}
placeholder="search_verb"
type="search"
value="bar"
<i
className="icon-search-new"
/>
</div>
<ul
className="menu"
>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"name": "foobar",
"status": Object {
"bugs": 0,
"codeSmells": 0,
"vulnerabilities": 0,
},
"type": "SHORT",
}
</button>
<input
autoFocus={true}
className="search-box-input"
onChange={[Function]}
onKeyDown={[Function]}
placeholder="search_verb"
type="search"
value="bar"
/>
</div>
<ul
className="menu"
>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"isOrphan": undefined,
"mergeBranch": "master",
"name": "foobar",
"status": Object {
"bugs": 0,
"codeSmells": 0,
"vulnerabilities": 0,
},
"type": "SHORT",
}
component={
Object {
"key": "component",
}
}
component={
Object {
"key": "component",
}
onSelect={[Function]}
selected={true}
/>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"name": "bar",
"type": "LONG",
}
}
onSelect={[Function]}
selected={true}
/>
<li
className="divider"
/>
<ComponentNavBranchesMenuItem
branch={
Object {
"isMain": false,
"name": "bar",
"type": "LONG",
}
component={
Object {
"key": "component",
}
}
component={
Object {
"key": "component",
}
onSelect={[Function]}
selected={false}
/>
</ul>
</div>
}
onSelect={[Function]}
selected={false}
/>
</ul>
</div>
`;

+ 85
- 2
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap View File

@@ -19,6 +19,12 @@ exports[`renders main branch 1`] = `
>
<div>
<BranchIcon
branch={
Object {
"isMain": true,
"name": "master",
}
}
className="little-spacer-right"
/>
master
@@ -30,8 +36,7 @@ exports[`renders main branch 1`] = `
branch={
Object {
"isMain": true,
"name": undefined,
"type": "LONG",
"name": "master",
}
}
concise={true}
@@ -62,6 +67,19 @@ exports[`renders short-living branch 1`] = `
>
<div>
<BranchIcon
branch={
Object {
"isMain": false,
"mergeBranch": "master",
"name": "foo",
"status": Object {
"bugs": 1,
"codeSmells": 2,
"vulnerabilities": 3,
},
"type": "SHORT",
}
}
className="little-spacer-right big-spacer-left"
/>
foo
@@ -73,6 +91,71 @@ exports[`renders short-living branch 1`] = `
branch={
Object {
"isMain": false,
"mergeBranch": "master",
"name": "foo",
"status": Object {
"bugs": 1,
"codeSmells": 2,
"vulnerabilities": 3,
},
"type": "SHORT",
}
}
concise={true}
/>
</div>
</Link>
</li>
`;

exports[`renders short-living orhpan branch 1`] = `
<li
onMouseEnter={[Function]}
>
<Link
className="navbar-context-meta-branch-menu-item"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"branch": "foo",
"id": "component",
"resolved": "false",
},
}
}
>
<div>
<BranchIcon
branch={
Object {
"isMain": false,
"isOrphan": true,
"mergeBranch": "master",
"name": "foo",
"status": Object {
"bugs": 1,
"codeSmells": 2,
"vulnerabilities": 3,
},
"type": "SHORT",
}
}
className="little-spacer-right"
/>
foo
</div>
<div
className="big-spacer-left note"
>
<BranchStatus
branch={
Object {
"isMain": false,
"isOrphan": true,
"mergeBranch": "master",
"name": "foo",
"status": Object {
"bugs": 1,

+ 144
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap View File

@@ -1,5 +1,143 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should work for long-living branches 1`] = `
<NavBarTabs>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
overview.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"branch": "release",
"id": "foo",
"resolved": "false",
},
}
}
>
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
layout.measures
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/code",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
code.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/activity",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
project_activity.page
</Link>
</li>
</NavBarTabs>
`;

exports[`should work for short-living branches 1`] = `
<NavBarTabs>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"branch": "feature",
"id": "foo",
"resolved": "false",
},
}
}
>
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/code",
"query": Object {
"branch": "feature",
"id": "foo",
},
}
}
>
code.page
</Link>
</li>
</NavBarTabs>
`;

exports[`should work with extensions 1`] = `
<NavBarTabs>
<li>
@@ -11,6 +149,7 @@ exports[`should work with extensions 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -47,6 +186,7 @@ exports[`should work with extensions 1`] = `
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -82,6 +222,7 @@ exports[`should work with extensions 1`] = `
Object {
"pathname": "/project/activity",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -212,6 +353,7 @@ exports[`should work with multiple extensions 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -248,6 +390,7 @@ exports[`should work with multiple extensions 1`] = `
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -283,6 +426,7 @@ exports[`should work with multiple extensions 1`] = `
Object {
"pathname": "/project/activity",
"query": Object {
"branch": undefined,
"id": "foo",
},
}

+ 41
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap View File

@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders nothing for long-living branch 1`] = `
<div
className="navbar-context-meta"
>
<ul
className="list-inline"
/>
</div>
`;

exports[`renders status of short-living branch 1`] = `
<div
className="navbar-context-meta"
>
<ul
className="list-inline"
>
<li
className="navbar-context-meta-branch"
>
<BranchStatus
branch={
Object {
"isMain": false,
"mergeBranch": "master",
"name": "feature",
"status": Object {
"bugs": 0,
"codeSmells": 2,
"vulnerabilities": 3,
},
"type": "SHORT",
}
}
/>
</li>
</ul>
</div>
`;

+ 8
- 0
server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap View File

@@ -19,6 +19,7 @@ exports[`renders favorite 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -65,6 +66,7 @@ exports[`renders match 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -110,6 +112,7 @@ exports[`renders organizations 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -160,6 +163,7 @@ exports[`renders organizations 2`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -205,6 +209,7 @@ exports[`renders projects 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "qwe",
},
}
@@ -255,6 +260,7 @@ exports[`renders recently browsed 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -300,6 +306,7 @@ exports[`renders selected 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
@@ -344,6 +351,7 @@ exports[`renders selected 2`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}

+ 1
- 0
server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap View File

@@ -22,6 +22,7 @@ exports[`should match snapshot 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}

server/sonar-web/src/main/js/apps/code/bucket.js → server/sonar-web/src/main/js/apps/code/bucket.ts View File

@@ -17,35 +17,54 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
let bucket = {};
let childrenBucket = {};
let breadcrumbsBucket = {};
import { Breadcrumb, Component } from './types';

export function addComponent(component) {
let bucket: { [key: string]: Component } = {};
let childrenBucket: {
[key: string]: {
children: Component[];
page: number;
total: number;
};
} = {};
let breadcrumbsBucket: { [key: string]: Breadcrumb[] } = {};

export function addComponent(component: Component): void {
bucket[component.key] = component;
}

export function getComponent(componentKey) {
export function getComponent(componentKey: string): Component {
return bucket[componentKey];
}

export function addComponentChildren(componentKey, children, total, page) {
export function addComponentChildren(
componentKey: string,
children: Component[],
total: number,
page: number
): void {
childrenBucket[componentKey] = { children, total, page };
}

export function getComponentChildren(componentKey) {
export function getComponentChildren(
componentKey: string
): {
children: Component[];
page: number;
total: number;
} {
return childrenBucket[componentKey];
}

export function addComponentBreadcrumbs(componentKey, breadcrumbs) {
export function addComponentBreadcrumbs(componentKey: string, breadcrumbs: Breadcrumb[]): void {
breadcrumbsBucket[componentKey] = breadcrumbs;
}

export function getComponentBreadcrumbs(componentKey) {
export function getComponentBreadcrumbs(componentKey: string): Breadcrumb[] {
return breadcrumbsBucket[componentKey];
}

export function clearBucket() {
export function clearBucket(): void {
bucket = {};
childrenBucket = {};
breadcrumbsBucket = {};

server/sonar-web/src/main/js/apps/code/components/App.js → server/sonar-web/src/main/js/apps/code/components/App.tsx View File

@@ -17,11 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
import React from 'react';
import * as classNames from 'classnames';
import * as React from 'react';
import Helmet from 'react-helmet';
import Components from './Components';
import Breadcrumbs from './Breadcrumbs';
import { Component as CodeComponent } from '../types';
import SourceViewer from './../../../components/SourceViewer/SourceViewer';
import Search from './Search';
import ListFooter from '../../../components/controls/ListFooter';
@@ -32,19 +33,36 @@ import {
parseError
} from '../utils';
import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket';
import { getBranchName } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
import '../code.css';
import { Component, Branch } from '../../../app/types';

export default class App extends React.PureComponent {
state = {
interface Props {
branch: Branch;
component: Component;
location: { query: { [x: string]: string } };
}

interface State {
baseComponent?: CodeComponent;
breadcrumbs: Array<CodeComponent>;
components?: Array<CodeComponent>;
error?: string;
loading: boolean;
page: number;
searchResults?: Array<CodeComponent>;
sourceViewer?: CodeComponent;
total: number;
}

export default class App extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = {
loading: true,
baseComponent: null,
components: null,
breadcrumbs: [],
total: 0,
page: 0,
sourceViewer: null,
error: null
page: 0
};

componentDidMount() {
@@ -52,8 +70,8 @@ export default class App extends React.PureComponent {
this.handleComponentChange();
}

componentDidUpdate(prevProps) {
if (prevProps.component !== this.props.component) {
componentDidUpdate(prevProps: Props) {
if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) {
this.handleComponentChange();
} else if (prevProps.location !== this.props.location) {
this.handleUpdate();
@@ -66,31 +84,31 @@ export default class App extends React.PureComponent {
}

handleComponentChange() {
const { component } = this.props;
const { branch, component } = this.props;

// we already know component's breadcrumbs,
addComponentBreadcrumbs(component.key, component.breadcrumbs);

this.setState({ loading: true });
const isPortfolio = ['VW', 'SVW'].includes(component.qualifier);
retrieveComponentChildren(component.key, isPortfolio, component.branch)
.then(r => {
addComponent(r.baseComponent);
retrieveComponentChildren(component.key, isPortfolio, getBranchName(branch))
.then(() => {
addComponent(component);
this.handleUpdate();
})
.catch(e => {
if (this.mounted) {
this.setState({ loading: false });
parseError(e).then(this.handleError.bind(this));
parseError(e).then(this.handleError);
}
});
}

loadComponent(componentKey) {
loadComponent(componentKey: string) {
this.setState({ loading: true });

const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
retrieveComponent(componentKey, isPortfolio, this.props.component.branch)
retrieveComponent(componentKey, isPortfolio, getBranchName(this.props.branch))
.then(r => {
if (this.mounted) {
if (['FIL', 'UTS'].includes(r.component.qualifier)) {
@@ -98,7 +116,7 @@ export default class App extends React.PureComponent {
loading: false,
sourceViewer: r.component,
breadcrumbs: r.breadcrumbs,
searchResults: null
searchResults: undefined
});
} else {
this.setState({
@@ -108,8 +126,8 @@ export default class App extends React.PureComponent {
breadcrumbs: r.breadcrumbs,
total: r.total,
page: r.page,
sourceViewer: null,
searchResults: null
sourceViewer: undefined,
searchResults: undefined
});
}
}
@@ -117,7 +135,7 @@ export default class App extends React.PureComponent {
.catch(e => {
if (this.mounted) {
this.setState({ loading: false });
parseError(e).then(this.handleError.bind(this));
parseError(e).then(this.handleError);
}
});
}
@@ -131,13 +149,16 @@ export default class App extends React.PureComponent {
}

handleLoadMore = () => {
const { baseComponent, page } = this.state;
const { baseComponent, components, page } = this.state;
if (!baseComponent || !components) {
return;
}
const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
loadMoreChildren(baseComponent.key, page + 1, isPortfolio, this.props.component.branch)
loadMoreChildren(baseComponent.key, page + 1, isPortfolio, getBranchName(this.props.branch))
.then(r => {
if (this.mounted) {
this.setState({
components: [...this.state.components, ...r.components],
components: [...components, ...r.components],
page: r.page,
total: r.total
});
@@ -151,14 +172,14 @@ export default class App extends React.PureComponent {
});
};

handleError = error => {
handleError = (error: string) => {
if (this.mounted) {
this.setState({ error });
}
};

render() {
const { component, location } = this.props;
const { branch, component, location } = this.props;
const {
loading,
error,
@@ -168,10 +189,9 @@ export default class App extends React.PureComponent {
total,
sourceViewer
} = this.state;
const branchName = getBranchName(branch);

const shouldShowSourceViewer = !!sourceViewer;
const shouldShowComponents = !shouldShowSourceViewer && components;
const shouldShowBreadcrumbs = Array.isArray(breadcrumbs) && breadcrumbs.length > 1;
const shouldShowBreadcrumbs = breadcrumbs.length > 1;

const componentsClassName = classNames('spacer-top', { 'new-loading': loading });

@@ -184,27 +204,35 @@ export default class App extends React.PureComponent {
{error}
</div>}

<Search location={location} component={component} onError={this.handleError} />
<Search
branch={branchName}
component={component}
location={location}
onError={this.handleError}
/>

<div className="code-components">
{shouldShowBreadcrumbs &&
<Breadcrumbs rootComponent={component} breadcrumbs={breadcrumbs} />}
<Breadcrumbs branch={branchName} breadcrumbs={breadcrumbs} rootComponent={component} />}

{shouldShowComponents &&
{sourceViewer == undefined &&
components != undefined &&
<div className={componentsClassName}>
<Components
rootComponent={component}
baseComponent={baseComponent}
branch={branchName}
components={components}
rootComponent={component}
/>
</div>}

{shouldShowComponents &&
{sourceViewer == undefined &&
components != undefined &&
<ListFooter count={components.length} total={total} loadMore={this.handleLoadMore} />}

{shouldShowSourceViewer &&
{sourceViewer != undefined &&
<div className="spacer-top">
<SourceViewer branch={component.branch} component={sourceViewer.key} />
<SourceViewer branch={branchName} component={sourceViewer.key} />
</div>}
</div>
</div>

server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js → server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx View File

@@ -17,18 +17,26 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import Breadcrumb from './Breadcrumb';
import * as React from 'react';
import ComponentName from './ComponentName';
import { Component } from '../types';

export default function Breadcrumbs({ rootComponent, breadcrumbs }) {
interface Props {
branch?: string;
breadcrumbs: Component[];
rootComponent: Component;
}

export default function Breadcrumbs({ branch, breadcrumbs, rootComponent }: Props) {
return (
<ul className="code-breadcrumbs">
{breadcrumbs.map((component, index) =>
<li key={component.key}>
<Breadcrumb
rootComponent={rootComponent}
component={component}
<ComponentName
branch={branch}
canBrowse={index < breadcrumbs.length - 1}
component={component}
rootComponent={rootComponent}
/>
</li>
)}

server/sonar-web/src/main/js/apps/code/components/Component.js → server/sonar-web/src/main/js/apps/code/components/Component.tsx View File

@@ -17,18 +17,29 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
import React from 'react';
import ReactDOM from 'react-dom';
import * as classNames from 'classnames';
import * as React from 'react';
import ComponentName from './ComponentName';
import ComponentMeasure from './ComponentMeasure';
import ComponentDetach from './ComponentDetach';
import ComponentPin from './ComponentPin';
import { Component as IComponent } from '../types';

const TOP_OFFSET = 200;
const BOTTOM_OFFSET = 10;

export default class Component extends React.PureComponent {
interface Props {
branch?: string;
canBrowse?: boolean;
component: IComponent;
previous?: IComponent;
rootComponent: IComponent;
selected?: boolean;
}

export default class Component extends React.PureComponent<Props> {
node: HTMLElement;

componentDidMount() {
this.handleUpdate();
}
@@ -49,8 +60,7 @@ export default class Component extends React.PureComponent {
}

handleScroll() {
const node = ReactDOM.findDOMNode(this);
const position = node.getBoundingClientRect();
const position = this.node.getBoundingClientRect();
const { top, bottom } = position;
if (bottom > window.innerHeight - BOTTOM_OFFSET) {
window.scrollTo(0, bottom - window.innerHeight + window.scrollY + BOTTOM_OFFSET);
@@ -60,7 +70,14 @@ export default class Component extends React.PureComponent {
}

render() {
const { component, rootComponent, selected, previous, canBrowse } = this.props;
const {
branch,
component,
rootComponent,
selected = false,
previous,
canBrowse = false
} = this.props;
const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier);
const isApplication = rootComponent.qualifier === 'APP';

@@ -70,10 +87,10 @@ export default class Component extends React.PureComponent {
switch (component.qualifier) {
case 'FIL':
case 'UTS':
componentAction = <ComponentPin branch={rootComponent.branch} component={component} />;
componentAction = <ComponentPin branch={branch} component={component} />;
break;
default:
componentAction = <ComponentDetach branch={rootComponent.branch} component={component} />;
componentAction = <ComponentDetach branch={branch} component={component} />;
}
}

@@ -93,10 +110,10 @@ export default class Component extends React.PureComponent {
{ metric: 'code_smells', type: 'SHORT_INT' },
{ metric: 'coverage', type: 'PERCENT' },
{ metric: 'duplicated_lines_density', type: 'PERCENT' }
].filter(Boolean);
].filter(Boolean) as Array<{ metric: string; type: string }>;

return (
<tr className={classNames({ selected })}>
<tr className={classNames({ selected })} ref={node => (this.node = node as HTMLElement)}>
<td className="thin nowrap">
<span className="spacer-right">
{componentAction}
@@ -104,6 +121,7 @@ export default class Component extends React.PureComponent {
</td>
<td className="code-name-cell">
<ComponentName
branch={branch}
component={component}
rootComponent={rootComponent}
previous={previous}

server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js → server/sonar-web/src/main/js/apps/code/components/ComponentDetach.tsx View File

@@ -17,11 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import { Link } from 'react-router';
import { translate } from '../../../helpers/l10n';
import { Component } from '../types';

export default function ComponentDetach({ component, branch }) {
interface Props {
branch?: string;
component: Component;
}

export default function ComponentDetach({ component, branch }: Props) {
return (
<Link
to={{ pathname: '/dashboard', query: { branch, id: component.refKey || component.key } }}

server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js → server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx View File

@@ -17,10 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import Measure from '../../../components/measure/Measure';
import { Component } from '../types';

const ComponentMeasure = ({ component, metricKey, metricType }) => {
interface Props {
component: Component;
metricKey: string;
metricType: string;
}

export default function ComponentMeasure({ component, metricKey, metricType }: Props) {
const isProject = component.qualifier === 'TRK';
const isReleasability = metricKey === 'releasability_rating';

@@ -35,9 +42,10 @@ const ComponentMeasure = ({ component, metricKey, metricType }) => {
return <span />;
}

// TODO
const AnyMeasure = Measure as any;

return (
<Measure measure={{ ...measure, metric: { key: finalMetricKey, type: finalMetricType } }} />
<AnyMeasure measure={{ ...measure, metric: { key: finalMetricKey, type: finalMetricType } }} />
);
};

export default ComponentMeasure;
}

server/sonar-web/src/main/js/apps/code/components/ComponentName.js → server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx View File

@@ -17,12 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import { Link } from 'react-router';
import Truncated from './Truncated';
import QualifierIcon from '../../../components/shared/QualifierIcon';
import { Component } from '../types';

function getTooltip(component) {
function getTooltip(component: Component) {
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS';
if (isFile && component.path) {
return component.path + '\n\n' + component.key;
@@ -31,7 +32,7 @@ function getTooltip(component) {
}
}

function mostCommitPrefix(strings) {
function mostCommitPrefix(strings: string[]) {
const sortedStrings = strings.slice(0).sort();
const firstString = sortedStrings[0];
const firstStringLength = firstString.length;
@@ -46,9 +47,21 @@ function mostCommitPrefix(strings) {
return prefix.substr(0, prefix.length - lastPrefixPart.length);
}

const ComponentName = ({ component, rootComponent, previous, canBrowse }) => {
interface Props {
branch?: string;
canBrowse?: boolean;
component: Component;
previous?: Component;
rootComponent: Component;
}

export default function ComponentName(props: Props) {
const { branch, component, rootComponent, previous, canBrowse = false } = props;
const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR';
const prefix = areBothDirs ? mostCommitPrefix([component.name + '/', previous.name + '/']) : '';
const prefix =
areBothDirs && previous != undefined
? mostCommitPrefix([component.name + '/', previous.name + '/'])
: '';
const name = prefix
? <span>
<span style={{ color: '#777' }}>
@@ -71,7 +84,7 @@ const ComponentName = ({ component, rootComponent, previous, canBrowse }) => {
</Link>
);
} else if (canBrowse) {
const query = { id: rootComponent.key, branch: rootComponent.branch };
const query = { id: rootComponent.key, branch };
if (component.key !== rootComponent.key) {
Object.assign(query, { selected: component.key });
}
@@ -93,6 +106,4 @@ const ComponentName = ({ component, rootComponent, previous, canBrowse }) => {
{inner}
</Truncated>
);
};

export default ComponentName;
}

server/sonar-web/src/main/js/apps/code/components/ComponentPin.js → server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx View File

@@ -17,14 +17,20 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import Workspace from '../../../components/workspace/main';
import PinIcon from '../../../components/shared/pin-icon';
import { translate } from '../../../helpers/l10n';
import { Component } from '../types';

const ComponentPin = ({ branch, component }) => {
const handleClick = e => {
e.preventDefault();
interface Props {
branch?: string;
component: Component;
}

export default function ComponentPin({ branch, component }: Props) {
const handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
Workspace.openComponent({ branch, key: component.key });
};

@@ -37,6 +43,4 @@ const ComponentPin = ({ branch, component }) => {
<PinIcon />
</a>
);
};

export default ComponentPin;
}

server/sonar-web/src/main/js/apps/code/components/Components.js → server/sonar-web/src/main/js/apps/code/components/Components.tsx View File

@@ -17,36 +17,48 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import Component from './Component';
import ComponentsEmpty from './ComponentsEmpty';
import ComponentsHeader from './ComponentsHeader';
import { Component as IComponent } from '../types';

export default function Components({ rootComponent, baseComponent, components, selected }) {
interface Props {
baseComponent?: IComponent;
branch?: string;
components: IComponent[];
rootComponent: IComponent;
selected?: IComponent;
}

export default function Components(props: Props) {
const { baseComponent, branch, components, rootComponent, selected } = props;
return (
<table className="data zebra">
<ComponentsHeader baseComponent={baseComponent} rootComponent={rootComponent} />
{baseComponent &&
<tbody>
<Component
branch={branch}
component={baseComponent}
key={baseComponent.key}
rootComponent={rootComponent}
component={baseComponent}
/>
<tr className="blank">
<td colSpan="8">&nbsp;</td>
<td colSpan={8}>&nbsp;</td>
</tr>
</tbody>}
<tbody>
{components.length
? components.map((component, index, list) =>
<Component
branch={branch}
canBrowse={true}
component={component}
key={component.key}
previous={index > 0 ? list[index - 1] : undefined}
rootComponent={rootComponent}
component={component}
selected={component === selected}
previous={index > 0 ? list[index - 1] : null}
canBrowse={true}
/>
)
: <ComponentsEmpty />}

server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js → server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx View File

@@ -17,16 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';

export default function ComponentsEmpty() {
return (
<tr>
<td colSpan="2">
<td colSpan={2}>
{translate('no_results')}
</td>
<td colSpan="6" />
<td colSpan={6} />
</tr>
);
}

server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js → server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx View File

@@ -17,10 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { Component } from '../types';

const ComponentsHeader = ({ baseComponent, rootComponent }) => {
interface Props {
baseComponent?: Component;
rootComponent: Component;
}

export default function ComponentsHeader({ baseComponent, rootComponent }: Props) {
const isPortfolio = rootComponent.qualifier === 'VW' || rootComponent.qualifier === 'SVW';
const isApplication = rootComponent.qualifier === 'APP';

@@ -40,7 +46,7 @@ const ComponentsHeader = ({ baseComponent, rootComponent }) => {
translate('metric', 'code_smells', 'name'),
translate('metric', 'coverage', 'name'),
translate('metric', 'duplicated_lines_density', 'short_name')
].filter(Boolean);
].filter(Boolean) as string[];

return (
<thead>
@@ -55,6 +61,4 @@ const ComponentsHeader = ({ baseComponent, rootComponent }) => {
</tr>
</thead>
);
};

export default ComponentsHeader;
}

server/sonar-web/src/main/js/apps/code/components/Search.js → server/sonar-web/src/main/js/apps/code/components/Search.tsx View File

@@ -17,32 +17,42 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import * as classNames from 'classnames';
import { debounce } from 'lodash';
import Components from './Components';
import { getTree } from '../../../api/components';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { parseError } from '../utils';
import { getComponentUrl } from '../../../helpers/urls';
import { getProjectUrl } from '../../../helpers/urls';
import { Component } from '../types';

interface Props {
branch?: string;
component: Component;
location: {};
onError: (error: string) => void;
}

interface State {
query: string;
loading: boolean;
results?: Component[];
selectedIndex?: number;
}

export default class Search extends React.PureComponent<Props, State> {
input: HTMLInputElement;
mounted: boolean;

export default class Search extends React.PureComponent {
static contextTypes = {
router: PropTypes.object.isRequired
};

static propTypes = {
component: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
onError: PropTypes.func.isRequired
};

state = {
state: State = {
query: '',
loading: false,
results: null,
selectedIndex: null
loading: false
};

componentWillMount() {
@@ -51,17 +61,17 @@ export default class Search extends React.PureComponent {

componentDidMount() {
this.mounted = true;
this.refs.input.focus();
this.input.focus();
}

componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps: Props) {
// if the url has change, reset the current state
if (nextProps.location !== this.props.location) {
this.setState({
query: '',
loading: false,
results: null,
selectedIndex: null
results: undefined,
selectedIndex: undefined
});
}
}
@@ -70,8 +80,8 @@ export default class Search extends React.PureComponent {
this.mounted = false;
}

checkInputValue(query) {
return this.refs.input.value === query;
checkInputValue(query: string) {
return this.input.value === query;
}

handleSelectNext() {
@@ -89,27 +99,23 @@ export default class Search extends React.PureComponent {
}

handleSelectCurrent() {
const { component } = this.props;
const { branch, component } = this.props;
const { results, selectedIndex } = this.state;
if (results != null && selectedIndex != null) {
const selected = results[selectedIndex];

if (selected.refKey) {
window.location = getComponentUrl(selected.refKey);
this.context.router.push(getProjectUrl(selected.refKey));
} else {
this.context.router.push({
pathname: '/code',
query: {
branch: component.branch,
id: component.key,
selected: selected.key
}
query: { branch, id: component.key, selected: selected.key }
});
}
}
}

handleKeyDown(e) {
handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
switch (e.keyCode) {
case 13:
e.preventDefault();
@@ -127,27 +133,22 @@ export default class Search extends React.PureComponent {
}
}

handleSearch = query => {
handleSearch = (query: string) => {
// first time check if value has changed due to debounce
if (this.mounted && this.checkInputValue(query)) {
const { component, onError } = this.props;
const { branch, component, onError } = this.props;
this.setState({ loading: true });

const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier);
const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL';

getTree(component.key, {
branch: component.branch,
q: query,
s: 'qualifier,name',
qualifiers
})
getTree(component.key, { branch, q: query, s: 'qualifier,name', qualifiers })
.then(r => {
// second time check if value has change due to api request
if (this.mounted && this.checkInputValue(query)) {
this.setState({
results: r.components,
selectedIndex: r.components.length > 0 ? 0 : null,
selectedIndex: r.components.length > 0 ? 0 : undefined,
loading: false
});
}
@@ -162,30 +163,30 @@ export default class Search extends React.PureComponent {
}
};

handleQueryChange(query) {
handleQueryChange(query: string) {
this.setState({ query });
if (query.length < 3) {
this.setState({ results: null });
this.setState({ results: undefined });
} else {
this.handleSearch(query);
}
}

handleInputChange(e) {
const query = e.target.value;
handleInputChange(event: React.SyntheticEvent<HTMLInputElement>) {
const query = event.currentTarget.value;
this.handleQueryChange(query);
}

handleSubmit(e) {
e.preventDefault();
const query = this.refs.input.value;
handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
event.preventDefault();
const query = this.input.value;
this.handleQueryChange(query);
}

render() {
const { component } = this.props;
const { query, loading, selectedIndex, results } = this.state;
const selected = selectedIndex != null && results != null ? results[selectedIndex] : null;
const selected = selectedIndex != null && results != null ? results[selectedIndex] : undefined;
const containerClassName = classNames('code-search', {
'code-search-with-results': results != null
});
@@ -201,7 +202,7 @@ export default class Search extends React.PureComponent {
</button>

<input
ref="input"
ref={node => (this.input = node as HTMLInputElement)}
onKeyDown={this.handleKeyDown.bind(this)}
onChange={this.handleInputChange.bind(this)}
value={query}
@@ -209,7 +210,7 @@ export default class Search extends React.PureComponent {
type="search"
name="q"
placeholder={translate('search_verb')}
maxLength="100"
maxLength={100}
autoComplete="off"
/>

@@ -221,7 +222,12 @@ export default class Search extends React.PureComponent {
</form>

{results != null &&
<Components rootComponent={component} components={results} selected={selected} />}
<Components
branch={this.props.branch}
components={results}
rootComponent={component}
selected={selected}
/>}
</div>
);
}

server/sonar-web/src/main/js/apps/code/components/Truncated.js → server/sonar-web/src/main/js/apps/code/components/Truncated.tsx View File

@@ -17,9 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';

export default function Truncated({ children, title }) {
interface Props {
children: React.ReactNode;
title: string;
}

export default function Truncated({ children, title }: Props) {
return (
<span className="code-truncated" title={title}>
{children}

server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js → server/sonar-web/src/main/js/apps/code/types.ts View File

@@ -1,7 +1,7 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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
@@ -17,11 +17,25 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import ComponentName from './ComponentName';
interface Measure {
metric: string;
value: string;
periods?: Period[];
}

interface Period {
index: number;
value: string;
}

export interface Component extends Breadcrumb {
measures?: Measure[];
path?: string;
refKey?: string;
}

export default function Breadcrumb({ rootComponent, component, canBrowse }) {
return (
<ComponentName rootComponent={rootComponent} component={component} canBrowse={canBrowse} />
);
export interface Breadcrumb {
key: string;
name: string;
qualifier: string;
}

server/sonar-web/src/main/js/apps/code/utils.js → server/sonar-web/src/main/js/apps/code/utils.ts View File

@@ -26,6 +26,7 @@ import {
addComponentBreadcrumbs,
getComponentBreadcrumbs
} from './bucket';
import { Breadcrumb, Component } from './types';
import { getChildren, getComponent, getBreadcrumbs } from '../../api/components';
import { translate } from '../../helpers/l10n';

@@ -50,7 +51,11 @@ const PORTFOLIO_METRICS = [

const PAGE_SIZE = 100;

function requestChildren(componentKey, metrics, page) {
function requestChildren(
componentKey: string,
metrics: string[],
page: number
): Promise<Component[]> {
return getChildren(componentKey, metrics, { p: page, ps: PAGE_SIZE }).then(r => {
if (r.paging.total > r.paging.pageSize * r.paging.pageIndex) {
return requestChildren(componentKey, metrics, page + 1).then(moreComponents => {
@@ -61,14 +66,24 @@ function requestChildren(componentKey, metrics, page) {
});
}

function requestAllChildren(componentKey, metrics) {
function requestAllChildren(componentKey: string, metrics: string[]): Promise<Component[]> {
return requestChildren(componentKey, metrics, 1);
}

function expandRootDir(metrics) {
interface Children {
components: Component[];
page: number;
total: number;
}

interface ExpandRootDirFunc {
(children: Children): Promise<Children>;
}

function expandRootDir(metrics: string[]): ExpandRootDirFunc {
return function({ components, total, ...other }) {
const rootDir = components.find(
component => component.qualifier === 'DIR' && component.name === '/'
(component: Component) => component.qualifier === 'DIR' && component.name === '/'
);
if (rootDir) {
return requestAllChildren(rootDir.key, metrics).then(rootDirComponents => {
@@ -77,31 +92,30 @@ function expandRootDir(metrics) {
return { components: nextComponents, total: nextTotal, ...other };
});
} else {
return { components, total, ...other };
return Promise.resolve({ components, total, ...other });
}
};
}

function prepareChildren(r) {
function prepareChildren(r: any): Children {
return {
components: r.components,
total: r.paging.total,
page: r.paging.pageIndex,
baseComponent: r.baseComponent
page: r.paging.pageIndex
};
}

function skipRootDir(breadcrumbs) {
function skipRootDir(breadcrumbs: Component[]) {
return breadcrumbs.filter(component => {
return !(component.qualifier === 'DIR' && component.name === '/');
});
}

function storeChildrenBase(children) {
function storeChildrenBase(children: Component[]) {
children.forEach(addComponent);
}

function storeChildrenBreadcrumbs(parentComponentKey, children) {
function storeChildrenBreadcrumbs(parentComponentKey: string, children: Breadcrumb[]) {
const parentBreadcrumbs = getComponentBreadcrumbs(parentComponentKey);
if (parentBreadcrumbs) {
children.forEach(child => {
@@ -111,16 +125,11 @@ function storeChildrenBreadcrumbs(parentComponentKey, children) {
}
}

function getMetrics(isPortfolio) {
function getMetrics(isPortfolio: boolean) {
return isPortfolio ? PORTFOLIO_METRICS : METRICS;
}

/**
* @param {string} componentKey
* @param {boolean} isPortfolio
* @returns {Promise}
*/
function retrieveComponentBase(componentKey, isPortfolio, branch) {
function retrieveComponentBase(componentKey: string, isPortfolio: boolean, branch?: string) {
const existing = getComponentFromBucket(componentKey);
if (existing) {
return Promise.resolve(existing);
@@ -134,12 +143,11 @@ function retrieveComponentBase(componentKey, isPortfolio, branch) {
});
}

/**
* @param {string} componentKey
* @param {boolean} isPortfolio
* @returns {Promise}
*/
export function retrieveComponentChildren(componentKey, isPortfolio, branch) {
export function retrieveComponentChildren(
componentKey: string,
isPortfolio: boolean,
branch?: string
): Promise<{ components: Component[]; page: number; total: number }> {
const existing = getComponentChildren(componentKey);
if (existing) {
return Promise.resolve({
@@ -162,7 +170,10 @@ export function retrieveComponentChildren(componentKey, isPortfolio, branch) {
});
}

function retrieveComponentBreadcrumbs(componentKey, branch) {
function retrieveComponentBreadcrumbs(
componentKey: string,
branch?: string
): Promise<Breadcrumb[]> {
const existing = getComponentBreadcrumbs(componentKey);
if (existing) {
return Promise.resolve(existing);
@@ -174,12 +185,17 @@ function retrieveComponentBreadcrumbs(componentKey, branch) {
});
}

/**
* @param {string} componentKey
* @param {boolean} isPortfolio
* @returns {Promise}
*/
export function retrieveComponent(componentKey, isPortfolio, branch) {
export function retrieveComponent(
componentKey: string,
isPortfolio: boolean,
branch?: string
): Promise<{
breadcrumbs: Component[];
component: Component;
components: Component[];
page: number;
total: number;
}> {
return Promise.all([
retrieveComponentBase(componentKey, isPortfolio, branch),
retrieveComponentChildren(componentKey, isPortfolio, branch),
@@ -195,7 +211,12 @@ export function retrieveComponent(componentKey, isPortfolio, branch) {
});
}

export function loadMoreChildren(componentKey, page, isPortfolio, branch) {
export function loadMoreChildren(
componentKey: string,
page: number,
isPortfolio: boolean,
branch?: string
): Promise<Children> {
const metrics = getMetrics(isPortfolio);

return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, p: page })
@@ -209,18 +230,14 @@ export function loadMoreChildren(componentKey, page, isPortfolio, branch) {
});
}

/**
* Parse response of failed request
* @param {Error} error
* @returns {Promise}
*/
export function parseError(error) {
/** Parse response of failed request */
export function parseError(error: { response: Response }): Promise<string> {
const DEFAULT_MESSAGE = translate('default_error_message');

try {
return error.response
.json()
.then(r => r.errors.map(error => error.msg).join('. '))
.then(r => r.errors.map((error: any) => error.msg).join('. '))
.catch(() => DEFAULT_MESSAGE);
} catch (ex) {
return Promise.resolve(DEFAULT_MESSAGE);

+ 11
- 4
server/sonar-web/src/main/js/apps/component-measures/components/App.js View File

@@ -25,6 +25,7 @@ import MeasureContentContainer from './MeasureContentContainer';
import MeasureOverviewContainer from './MeasureOverviewContainer';
import Sidebar from '../sidebar/Sidebar';
import { hasBubbleChart, parseQuery, serializeQuery } from '../utils';
import { getBranchName } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
/*:: import type { Component, Query, Period } from '../types'; */
/*:: import type { RawQuery } from '../../../helpers/query'; */
@@ -33,12 +34,14 @@ import { translate } from '../../../helpers/l10n';
import '../style.css';

/*:: type Props = {|
branch: {},
component: Component,
currentUser: { isLoggedIn: boolean },
location: { pathname: string, query: RawQuery },
fetchMeasures: (
component: string,
metricsKey: Array<string>
metricsKey: Array<string>,
branch: string | null
) => Promise<{ component: Component, measures: Array<MeasureEnhanced>, leakPeriod: ?Period }>,
fetchMetrics: () => void,
metrics: { [string]: Metric },
@@ -81,6 +84,7 @@ export default class App extends React.PureComponent {

componentWillReceiveProps(nextProps /*: Props */) {
if (
nextProps.branch !== this.props.branch ||
nextProps.component.key !== this.props.component.key ||
nextProps.metrics !== this.props.metrics
) {
@@ -97,12 +101,12 @@ export default class App extends React.PureComponent {
}
}

fetchMeasures = ({ component, fetchMeasures, metrics, metricsKey } /*: Props */) => {
fetchMeasures = ({ branch, component, fetchMeasures, metrics, metricsKey } /*: Props */) => {
this.setState({ loading: true });
const filteredKeys = metricsKey.filter(
key => !metrics[key].hidden && !['DATA', 'DISTRIB'].includes(metrics[key].type)
);
fetchMeasures(component.key, filteredKeys).then(
fetchMeasures(component.key, filteredKeys, getBranchName(branch)).then(
({ measures, leakPeriod }) => {
if (this.mounted) {
this.setState({
@@ -125,6 +129,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...query,
branch: getBranchName(this.props.branch),
id: this.props.component.key
}
});
@@ -135,7 +140,7 @@ export default class App extends React.PureComponent {
if (isLoading) {
return <i className="spinner spinner-margin" />;
}
const { component, fetchMeasures, metrics } = this.props;
const { branch, component, fetchMeasures, metrics } = this.props;
const { leakPeriod } = this.state;
const query = parseQuery(this.props.location.query);
const metric = metrics[query.metric];
@@ -159,6 +164,7 @@ export default class App extends React.PureComponent {

{metric != null &&
<MeasureContentContainer
branch={branch}
className="layout-page-main"
currentUser={this.props.currentUser}
rootComponent={component}
@@ -174,6 +180,7 @@ export default class App extends React.PureComponent {
{metric == null &&
hasBubbleChart(query.metric) &&
<MeasureOverviewContainer
branch={branch}
className="layout-page-main"
rootComponent={component}
currentUser={this.props.currentUser}

+ 9
- 5
server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js View File

@@ -47,15 +47,19 @@ function banQualityGate(component /*: Component */) /*: Array<Measure> */ {
return component.measures.filter(measure => !bannedMetrics.includes(measure.metric));
}

const fetchMeasures = (component /*: string */, metricsKey /*: Array<string> */) => (
dispatch,
getState
) => {
const fetchMeasures = (
component /*: string */,
metricsKey /*: Array<string> */,
branch /*: string | null */
) => (dispatch, getState) => {
if (metricsKey.length <= 0) {
return Promise.resolve({ component: {}, measures: [], leakPeriod: null });
}

return getMeasuresAndMeta(component, metricsKey, { additionalFields: 'periods' }).then(r => {
return getMeasuresAndMeta(component, metricsKey, {
additionalFields: 'periods',
branch
}).then(r => {
const measures = banQualityGate(r.component).map(measure =>
enhanceMeasure(measure, getMetrics(getState()))
);

+ 4
- 2
server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js View File

@@ -22,10 +22,12 @@ import React from 'react';
import key from 'keymaster';
import Breadcrumb from './Breadcrumb';
import { getBreadcrumbs } from '../../../api/components';
import { getBranchName } from '../../../helpers/branches';
/*:: import type { Component } from '../types'; */

/*:: type Props = {|
backToFirst: boolean,
branch: {},
className?: string,
component: Component,
handleSelect: string => void,
@@ -75,7 +77,7 @@ export default class Breadcrumbs extends React.PureComponent {
key.unbind('left', 'measures-files');
}

fetchBreadcrumbs = ({ component, rootComponent } /*: Props */) => {
fetchBreadcrumbs = ({ branch, component, rootComponent } /*: Props */) => {
const isRoot = component.key === rootComponent.key;
if (isRoot) {
if (this.mounted) {
@@ -83,7 +85,7 @@ export default class Breadcrumbs extends React.PureComponent {
}
return;
}
getBreadcrumbs(component.key).then(breadcrumbs => {
getBreadcrumbs(component.key, getBranchName(branch)).then(breadcrumbs => {
if (this.mounted) {
this.setState({ breadcrumbs });
}

+ 21
- 5
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js View File

@@ -32,6 +32,7 @@ import TreeMapView from '../drilldown/TreeMapView';
import { getComponentTree } from '../../../api/components';
import { complementary } from '../config/complementary';
import { enhanceComponent, isFileType, isViewType } from '../utils';
import { getBranchName } from '../../../helpers/branches';
import { getProjectUrl } from '../../../helpers/urls';
import { isDiffMetric } from '../../../helpers/measures';
import { parseDate } from '../../../helpers/dates';
@@ -43,6 +44,7 @@ import { parseDate } from '../../../helpers/dates';
// https://github.com/facebook/flow/issues/3147
// router: { push: ({ pathname: string, query?: RawQuery }) => void }
/*:: type Props = {|
branch: {},
className?: string,
component: Component,
currentUser: { isLoggedIn: boolean },
@@ -86,7 +88,11 @@ export default class MeasureContent extends React.PureComponent {
}

componentWillReceiveProps(nextProps /*: Props */) {
if (nextProps.component !== this.props.component || nextProps.metric !== this.props.metric) {
if (
nextProps.branch !== this.props.branch ||
nextProps.component !== this.props.component ||
nextProps.metric !== this.props.metric
) {
this.fetchComponents(nextProps);
}
}
@@ -110,7 +116,10 @@ export default class MeasureContent extends React.PureComponent {
) => {
const strategy = view === 'list' ? 'leaves' : 'children';
const metricKeys = [metric.key];
const opts /*: Object */ = { metricSortFilter: 'withMeasuresOnly' };
const opts /*: Object */ = {
branch: getBranchName(this.props.branch),
metricSortFilter: 'withMeasuresOnly'
};
const isDiff = isDiffMetric(metric.key);
if (isDiff) {
opts.metricPeriodSort = 1;
@@ -215,7 +224,7 @@ export default class MeasureContent extends React.PureComponent {
onSelectComponent = (componentKey /*: string */) => this.setState({ selected: componentKey });

renderCode() {
const { component, leakPeriod } = this.props;
const { branch, component, leakPeriod } = this.props;
const leakPeriodDate =
isDiffMetric(this.props.metric.key) && leakPeriod != null ? parseDate(leakPeriod.date) : null;

@@ -232,7 +241,11 @@ export default class MeasureContent extends React.PureComponent {
}
return (
<div className="measure-details-viewer">
<SourceViewer component={component.key} filterLine={filterLine} />
<SourceViewer
branch={getBranchName(branch)}
component={component.key}
filterLine={filterLine}
/>
</div>
);
}
@@ -244,6 +257,7 @@ export default class MeasureContent extends React.PureComponent {
const selectedIdx = this.getSelectedIndex();
return (
<FilesView
branch={this.props.branch}
components={this.state.components}
fetchMore={this.fetchMoreComponents}
handleOpen={this.onOpenComponent}
@@ -260,6 +274,7 @@ export default class MeasureContent extends React.PureComponent {
if (view === 'treemap') {
return (
<TreeMapView
branch={this.props.branch}
components={this.state.components}
handleSelect={this.onOpenComponent}
metric={metric}
@@ -272,7 +287,7 @@ export default class MeasureContent extends React.PureComponent {
}

render() {
const { component, currentUser, measure, metric, rootComponent, view } = this.props;
const { branch, component, currentUser, measure, metric, rootComponent, view } = this.props;
const isLoggedIn = currentUser && currentUser.isLoggedIn;
const isFile = isFileType(component);
const selectedIdx = this.getSelectedIndex();
@@ -286,6 +301,7 @@ export default class MeasureContent extends React.PureComponent {
<div className="layout-page-main-inner">
<Breadcrumbs
backToFirst={view === 'list'}
branch={branch}
className="measure-breadcrumbs spacer-right text-ellipsis"
component={component}
handleSelect={this.onOpenComponent}

+ 7
- 3
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js View File

@@ -20,18 +20,21 @@
// @flow
import React from 'react';
import MeasureContent from './MeasureContent';
import { getBranchName } from '../../../helpers/branches';
/*:: import type { Component, Period, Query } from '../types'; */
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
/*:: import type { Metric } from '../../../store/metrics/actions'; */
/*:: import type { RawQuery } from '../../../helpers/query'; */

/*:: type Props = {|
branch: {},
className?: string,
currentUser: { isLoggedIn: boolean },
rootComponent: Component,
fetchMeasures: (
component: string,
metricsKey: Array<string>
metricsKey: Array<string>,
branch: string | null
) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>,
leakPeriod?: Period,
metric: Metric,
@@ -87,7 +90,7 @@ export default class MeasureContentContainer extends React.PureComponent {
this.mounted = false;
}

fetchMeasure = ({ rootComponent, fetchMeasures, metric, selected } /*: Props */) => {
fetchMeasure = ({ branch, rootComponent, fetchMeasures, metric, selected } /*: Props */) => {
this.updateLoading({ measure: true });

const metricKeys = [metric.key];
@@ -99,7 +102,7 @@ export default class MeasureContentContainer extends React.PureComponent {
metricKeys.push('file_complexity_distribution');
}

fetchMeasures(selected || rootComponent.key, metricKeys).then(
fetchMeasures(selected || rootComponent.key, metricKeys, getBranchName(branch)).then(
({ component, measures }) => {
if (this.mounted) {
const measure = measures.find(measure => measure.metric.key === metric.key);
@@ -132,6 +135,7 @@ export default class MeasureContentContainer extends React.PureComponent {

return (
<MeasureContent
branch={this.props.branch}
className={this.props.className}
component={this.state.component}
currentUser={this.props.currentUser}

+ 8
- 4
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js View File

@@ -26,11 +26,13 @@ import MeasureFavoriteContainer from './MeasureFavoriteContainer';
import PageActions from './PageActions';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import { getComponentLeaves } from '../../../api/components';
import { getBranchName } from '../../../helpers/branches';
import { enhanceComponent, getBubbleMetrics, isFileType } from '../utils';
/*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */
/*:: import type { Metric } from '../../../store/metrics/actions'; */

/*:: type Props = {|
branch: {},
className?: string,
component: Component,
currentUser: { isLoggedIn: boolean },
@@ -78,7 +80,7 @@ export default class MeasureOverview extends React.PureComponent {
}

fetchComponents = (props /*: Props */) => {
const { component, domain, metrics } = props;
const { branch, component, domain, metrics } = props;
if (isFileType(component)) {
return this.setState({ components: [], paging: null });
}
@@ -88,6 +90,7 @@ export default class MeasureOverview extends React.PureComponent {
metricsKey.push(colors.map(metric => metric.key));
}
const options = {
branch: getBranchName(branch),
s: 'metric',
metricSort: size.key,
asc: false,
@@ -112,11 +115,11 @@ export default class MeasureOverview extends React.PureComponent {
};

renderContent() {
const { component } = this.props;
const { branch, component } = this.props;
if (isFileType(component)) {
return (
<div className="measure-details-viewer">
<SourceViewer component={component.key} />
<SourceViewer branch={getBranchName(branch)} component={component.key} />
</div>
);
}
@@ -133,7 +136,7 @@ export default class MeasureOverview extends React.PureComponent {
}

render() {
const { component, currentUser, leakPeriod, rootComponent } = this.props;
const { branch, component, currentUser, leakPeriod, rootComponent } = this.props;
const isLoggedIn = currentUser && currentUser.isLoggedIn;
const isFile = isFileType(component);
return (
@@ -143,6 +146,7 @@ export default class MeasureOverview extends React.PureComponent {
<div className="layout-page-main-inner">
<Breadcrumbs
backToFirst={true}
branch={branch}
className="measure-breadcrumbs spacer-right text-ellipsis"
component={component}
handleSelect={this.props.updateSelected}

+ 5
- 2
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js View File

@@ -21,6 +21,7 @@
import React from 'react';
import MeasureOverview from './MeasureOverview';
import { getComponentShow } from '../../../api/components';
import { getBranchName } from '../../../helpers/branches';
import { getProjectUrl } from '../../../helpers/urls';
import { isViewType } from '../utils';
/*:: import type { Component, Period, Query } from '../types'; */
@@ -28,6 +29,7 @@ import { isViewType } from '../utils';
/*:: import type { Metric } from '../../../store/metrics/actions'; */

/*:: type Props = {|
branch: {},
className?: string,
rootComponent: Component,
currentUser: { isLoggedIn: boolean },
@@ -80,14 +82,14 @@ export default class MeasureOverviewContainer extends React.PureComponent {
this.mounted = false;
}

fetchComponent = ({ rootComponent, selected } /*: Props */) => {
fetchComponent = ({ branch, rootComponent, selected } /*: Props */) => {
if (!selected || rootComponent.key === selected) {
this.setState({ component: rootComponent });
this.updateLoading({ component: false });
return;
}
this.updateLoading({ component: true });
getComponentShow(selected).then(
getComponentShow(selected, getBranchName(branch)).then(
({ component }) => {
if (this.mounted) {
this.setState({ component });
@@ -121,6 +123,7 @@ export default class MeasureOverviewContainer extends React.PureComponent {

return (
<MeasureOverview
branch={this.props.branch}
className={this.props.className}
component={this.state.component}
currentUser={this.props.currentUser}

+ 3
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js View File

@@ -34,6 +34,7 @@ jest.mock('../../../../api/components', () => ({
it('should display correctly for the list view', () => {
const wrapper = mount(
<Breadcrumbs
branch={{ isMain: true }}
component={{ key: 'bar', name: 'Bar' }}
handleSelect={() => {}}
rootComponent={{ key: 'foo', name: 'Foo' }}
@@ -46,6 +47,7 @@ it('should display correctly for the list view', () => {
it('should display only the root component', () => {
const wrapper = mount(
<Breadcrumbs
branch={{ isMain: true }}
component={{ key: 'foo', name: 'Foo' }}
handleSelect={() => {}}
rootComponent={{ key: 'foo', name: 'Foo' }}
@@ -58,6 +60,7 @@ it('should display only the root component', () => {
it.only('should load the breadcrumb from the api', () => {
const wrapper = mount(
<Breadcrumbs
branch={{ isMain: true }}
component={{ key: 'bar', name: 'Bar' }}
handleSelect={() => {}}
rootComponent={{ key: 'foo', name: 'Foo' }}

+ 1
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap View File

@@ -148,6 +148,7 @@ exports[`should render correctly 1`] = `
Object {
"pathname": "/project/activity",
"query": Object {
"branch": undefined,
"custom_metrics": "reliability_rating",
"graph": "custom",
"id": "foo",

+ 6
- 4
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js View File

@@ -20,11 +20,13 @@
// @flow
import React from 'react';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { getBranchName } from '../../../helpers/branches';
import { splitPath } from '../../../helpers/path';
import { getComponentUrl } from '../../../helpers/urls';
/*:: import type { ComponentEnhanced } from '../types'; */

/*:: type Props = {
branch: {},
component: ComponentEnhanced,
onClick: string => void
}; */
@@ -66,22 +68,22 @@ export default class ComponentCell extends React.PureComponent {
}

render() {
const { component } = this.props;
const { branch, component } = this.props;
return (
<td className="measure-details-component-cell">
<div className="text-ellipsis">
{component.refId == null
{component.refKey == null
? <a
id={'component-measures-component-link-' + component.key}
className="link-no-underline"
href={getComponentUrl(component.key)}
href={getComponentUrl(component.key, getBranchName(branch))}
onClick={this.handleClick}>
{this.renderInner()}
</a>
: <a
id={'component-measures-component-link-' + component.key}
className="link-no-underline"
href={getComponentUrl(component.refKey || component.key)}>
href={getComponentUrl(component.refKey, getBranchName(branch))}>
<span className="big-spacer-right">
<i className="icon-detach" />
</span>

+ 3
- 1
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js View File

@@ -27,6 +27,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n';
/*:: import type { Metric } from '../../../store/metrics/actions'; */

/*:: type Props = {|
branch: {},
components: Array<ComponentEnhanced>,
onClick: string => void,
metric: Metric,
@@ -35,7 +36,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n';
|}; */

export default function ComponentsList(
{ components, onClick, metrics, metric, selectedComponent } /*: Props */
{ branch, components, onClick, metrics, metric, selectedComponent } /*: Props */
) {
if (!components.length) {
return <EmptyResult />;
@@ -67,6 +68,7 @@ export default function ComponentsList(
{components.map(component =>
<ComponentsListRow
key={component.id}
branch={branch}
component={component}
otherMetrics={otherMetrics}
isSelected={component.key === selectedComponent}

+ 3
- 2
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js View File

@@ -26,6 +26,7 @@ import MeasureCell from './MeasureCell';
/*:: import type { Metric } from '../../../store/metrics/actions'; */

/*:: type Props = {|
branch: {},
component: ComponentEnhanced,
isSelected: boolean,
onClick: string => void,
@@ -34,7 +35,7 @@ import MeasureCell from './MeasureCell';
|}; */

export default function ComponentsListRow(props /*: Props */) {
const { component } = props;
const { branch, component } = props;
const otherMeasures = props.otherMetrics.map(metric => {
const measure = component.measures.find(measure => measure.metric.key === metric.key);
return { ...measure, metric };
@@ -44,7 +45,7 @@ export default function ComponentsListRow(props /*: Props */) {
});
return (
<tr className={rowClass}>
<ComponentCell component={component} onClick={props.onClick} />
<ComponentCell branch={branch} component={component} onClick={props.onClick} />

<MeasureCell component={component} metric={props.metric} />


+ 2
- 0
server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js View File

@@ -28,6 +28,7 @@ import { scrollToElement } from '../../../helpers/scrolling';
/*:: import type { Metric } from '../../../store/metrics/actions'; */

/*:: type Props = {|
branch: {},
components: Array<ComponentEnhanced>,
fetchMore: () => void,
handleSelect: string => void,
@@ -117,6 +118,7 @@ export default class ListView extends React.PureComponent {
return (
<div ref={elem => (this.listContainer = elem)}>
<ComponentsList
branch={this.props.branch}
components={this.props.components}
metrics={this.props.metrics}
metric={this.props.metric}

+ 4
- 2
server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js View File

@@ -26,6 +26,7 @@ import ColorGradientLegend from '../../../components/charts/ColorGradientLegend'
import EmptyResult from './EmptyResult';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import TreeMap from '../../../components/charts/TreeMap';
import { getBranchName } from '../../../helpers/branches';
import { translate, translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n';
import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
import { getComponentUrl } from '../../../helpers/urls';
@@ -34,6 +35,7 @@ import { getComponentUrl } from '../../../helpers/urls';
/*:: import type { TreeMapItem } from '../../../components/charts/TreeMap'; */

/*:: type Props = {|
branch: {},
components: Array<ComponentEnhanced>,
handleSelect: string => void,
metric: Metric
@@ -62,7 +64,7 @@ export default class TreeMapView extends React.PureComponent {
}
}

getTreemapComponents = ({ components, metric } /*: Props */) => {
getTreemapComponents = ({ branch, components, metric } /*: Props */) => {
const colorScale = this.getColorScale(metric);
return components
.map(component => {
@@ -93,7 +95,7 @@ export default class TreeMapView extends React.PureComponent {
sizeValue
),
label: component.name,
link: getComponentUrl(component.refKey || component.key)
link: getComponentUrl(component.refKey || component.key, getBranchName(branch))
};
})
.filter(Boolean);

+ 9
- 7
server/sonar-web/src/main/js/apps/issues/components/App.js View File

@@ -55,6 +55,7 @@ import {
} from '../utils'; */
import ListFooter from '../../../components/controls/ListFooter';
import EmptySearch from '../../../components/common/EmptySearch';
import { getBranchName } from '../../../helpers/branches';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { scrollToElement } from '../../../helpers/scrolling';
/*:: import type { Issue } from '../../../components/issue/types'; */
@@ -173,6 +174,7 @@ export default class App extends React.PureComponent {
const { query: prevQuery } = prevProps.location;
if (
prevProps.component !== this.props.component ||
prevProps.branch !== this.props.branch ||
!areQueriesEqual(prevQuery, query) ||
areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query)
) {
@@ -308,7 +310,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...serializeQuery(this.state.query),
branch: this.props.branch && this.props.branch.name,
branch: this.props.branch && getBranchName(this.props.branch),
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined,
open: issue
@@ -327,7 +329,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...serializeQuery(this.state.query),
branch: this.props.branch && this.props.branch.name,
branch: this.props.branch && getBranchName(this.props.branch),
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined,
open: undefined
@@ -363,7 +365,7 @@ export default class App extends React.PureComponent {
: undefined;

const parameters = {
branch: this.props.branch && this.props.branch.name,
branch: this.props.branch && getBranchName(this.props.branch),
componentKeys: component && component.key,
s: 'FILE_LINE',
...serializeQuery(query),
@@ -554,7 +556,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...serializeQuery({ ...this.state.query, ...changes }),
branch: this.props.branch && this.props.branch.name,
branch: this.props.branch && getBranchName(this.props.branch),
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined
}
@@ -570,7 +572,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }),
branch: this.props.branch && this.props.branch.name,
branch: this.props.branch && getBranchName(this.props.branch),
id: this.props.component && this.props.component.key,
myIssues: myIssues ? 'true' : undefined
}
@@ -597,7 +599,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...DEFAULT_QUERY,
branch: this.props.branch && this.props.branch.name,
branch: this.props.branch && getBranchName(this.props.branch),
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined
}
@@ -890,7 +892,7 @@ export default class App extends React.PureComponent {
<div>
{openIssue
? <IssuesSourceViewer
branch={this.props.branch}
branch={this.props.branch && getBranchName(this.props.branch)}
component={component}
openIssue={openIssue}
loadIssues={this.fetchIssuesForComponent}

+ 2
- 2
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js View File

@@ -26,7 +26,7 @@ import { scrollToElement } from '../../../helpers/scrolling';

/*::
type Props = {|
branch?: { name: string },
branch?: string,
component: Component,
loadIssues: (string, number, number) => Promise<*>,
onIssueChange: Issue => void,
@@ -86,7 +86,7 @@ export default class IssuesSourceViewer extends React.PureComponent {
<div ref={node => (this.node = node)}>
<SourceViewer
aroundLine={openIssue.textRange ? openIssue.textRange.endLine : undefined}
branch={this.props.branch && this.props.branch.name}
branch={this.props.branch}
component={openIssue.component}
displayAllIssues={true}
highlightedLocations={locations}

+ 8
- 2
server/sonar-web/src/main/js/apps/overview/components/App.js View File

@@ -28,7 +28,7 @@ import SourceViewer from '../../../components/SourceViewer/SourceViewer';

/*::
type Props = {
branch: {},
branch: { name: string },
component: {
analysisDate?: string,
id: string,
@@ -84,6 +84,12 @@ export default class App extends React.PureComponent {
return <EmptyOverview component={component} />;
}

return <OverviewApp component={component} onComponentChange={this.props.onComponentChange} />;
return (
<OverviewApp
branch={this.props.branch}
component={component}
onComponentChange={this.props.onComponentChange}
/>
);
}
}

+ 21
- 14
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js View File

@@ -36,11 +36,13 @@ import { getLeakPeriod } from '../../../helpers/periods';
import { getCustomGraph, getGraph } from '../../../helpers/storage';
import { METRICS, HISTORY_METRICS_LIST } from '../utils';
import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils';
import { getBranchName } from '../../../helpers/branches';
/*:: import type { Component, History, MeasuresList, Period } from '../types'; */
import '../styles.css';

/*::
type Props = {
branch: { name: string },
component: Component,
onComponentChange: {} => void
};
@@ -70,14 +72,12 @@ export default class OverviewApp extends React.PureComponent {
if (domElement) {
domElement.classList.add('dashboard-page');
}
this.loadMeasures(this.props.component.key).then(() => this.loadHistory(this.props.component));
this.loadMeasures().then(this.loadHistory);
}

componentDidUpdate(prevProps /*: Props */) {
if (this.props.component.key !== prevProps.component.key) {
this.loadMeasures(this.props.component.key).then(() =>
this.loadHistory(this.props.component)
);
if (this.props.component.key !== prevProps.component.key || this.props.branch !== prevProps.branch) {
this.loadMeasures().then(this.loadHistory);
}
}

@@ -89,11 +89,13 @@ export default class OverviewApp extends React.PureComponent {
}
}

loadMeasures(componentKey /*: string */) {
loadMeasures() {
const { branch, component } = this.props;
this.setState({ loading: true });

return getMeasuresAndMeta(componentKey, METRICS, {
additionalFields: 'metrics,periods'
return getMeasuresAndMeta(component.key, METRICS, {
additionalFields: 'metrics,periods',
branch: getBranchName(branch)
}).then(
r => {
if (this.mounted) {
@@ -113,14 +115,18 @@ export default class OverviewApp extends React.PureComponent {
);
}

loadHistory(component /*: Component */) {
loadHistory = () => {
const { branch, component } = this.props;

let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph());
if (!graphMetrics || graphMetrics.length <= 0) {
graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
}

const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics));
return getAllTimeMachineData(component.key, metrics).then(r => {
return getAllTimeMachineData(component.key, metrics, {
branch: getBranchName(branch)
}).then(r => {
if (this.mounted) {
const history /*: History */ = {};
r.measures.forEach(measure => {
@@ -134,7 +140,7 @@ export default class OverviewApp extends React.PureComponent {
this.setState({ history, historyStartDate });
}
}, throwGlobalError);
}
};

getApplicationLeakPeriod = () =>
this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null;
@@ -148,7 +154,7 @@ export default class OverviewApp extends React.PureComponent {
}

render() {
const { component } = this.props;
const { branch, component } = this.props;
const { loading, measures, periods, history, historyStartDate } = this.state;

if (loading) {
@@ -157,7 +163,7 @@ export default class OverviewApp extends React.PureComponent {

const leakPeriod =
component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods);
const domainProps = { component, measures, leakPeriod, history, historyStartDate };
const domainProps = { branch, component, measures, leakPeriod, history, historyStartDate };

return (
<div className="page page-limited">
@@ -165,7 +171,7 @@ export default class OverviewApp extends React.PureComponent {
<div className="overview-main page-main">
{component.qualifier === 'APP'
? <ApplicationQualityGate component={component} />
: <QualityGate component={component} measures={measures} />}
: <QualityGate branch={branch} component={component} measures={measures} />}

<div className="overview-domains-list">
<BugsAndVulnerabilities {...domainProps} />
@@ -177,6 +183,7 @@ export default class OverviewApp extends React.PureComponent {

<div className="page-sidebar-fixed">
<Meta
branch={branch}
component={component}
history={history}
measures={measures}

+ 13
- 2
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js View File

@@ -24,12 +24,14 @@ import Analysis from './Analysis';
import PreviewGraph from './PreviewGraph';
import { getMetrics } from '../../../api/metrics';
import { getProjectActivity } from '../../../api/projectActivity';
import { getBranchName } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
/*:: import type { Analysis as AnalysisType } from '../../projectActivity/types'; */
/*:: import type { History, Metric } from '../types'; */

/*::
type Props = {
branch: {},
history: ?History,
project: string,
qualifier: string,
@@ -70,7 +72,11 @@ export default class AnalysesList extends React.PureComponent {
fetchData() {
this.setState({ loading: true });
Promise.all([
getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }),
getProjectActivity({
branch: getBranchName(this.props.branch),
project: this.props.project,
ps: PAGE_SIZE
}),
getMetrics()
]).then(response => {
if (this.mounted) {
@@ -111,6 +117,7 @@ export default class AnalysesList extends React.PureComponent {
</h4>

<PreviewGraph
branch={this.props.branch}
history={this.props.history}
project={this.props.project}
metrics={this.state.metrics}
@@ -120,7 +127,11 @@ export default class AnalysesList extends React.PureComponent {
{this.renderList(analyses)}

<div className="spacer-top small">
<Link to={{ pathname: '/project/activity', query: { id: this.props.project } }}>
<Link
to={{
pathname: '/project/activity',
query: { id: this.props.project, branch: getBranchName(this.props.branch) }
}}>
{translate('show_more')}
</Link>
</div>

+ 6
- 1
server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js View File

@@ -31,6 +31,7 @@ import {
hasHistoryDataValue,
splitSeriesInGraphs
} from '../../projectActivity/utils';
import { getBranchName } from '../../../helpers/branches';
import { getCustomGraph, getGraph } from '../../../helpers/storage';
import { formatMeasure, getShortType } from '../../../helpers/measures';
import { translate } from '../../../helpers/l10n';
@@ -39,6 +40,7 @@ import { translate } from '../../../helpers/l10n';

/*::
type Props = {
branch: {},
history: ?History,
metrics: Array<Metric>,
project: string,
@@ -137,7 +139,10 @@ export default class PreviewGraph extends React.PureComponent {
};

handleClick = () => {
this.props.router.push({ pathname: '/project/activity', query: { id: this.props.project } });
this.props.router.push({
pathname: '/project/activity',
query: { id: this.props.project, branch: getBranchName(this.props.branch) }
});
};

updateTooltip = (

+ 5
- 3
server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js View File

@@ -26,12 +26,14 @@ import BugIcon from '../../../components/icons-components/BugIcon';
import LeakPeriodLegend from '../components/LeakPeriodLegend';
import VulnerabilityIcon from '../../../components/icons-components/VulnerabilityIcon';
import { getMetricName } from '../helpers/metrics';
import { getBranchName } from '../../../helpers/branches';
import { getComponentDrilldownUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';

class BugsAndVulnerabilities extends React.PureComponent {
renderHeader() {
const { component } = this.props;
const { branch, component } = this.props;
const branchName = getBranchName(branch);

return (
<div className="overview-card-header">
@@ -41,7 +43,7 @@ class BugsAndVulnerabilities extends React.PureComponent {
</span>
<Link
className="button button-small button-compact spacer-left text-text-bottom"
to={getComponentDrilldownUrl(component.key, 'Reliability')}>
to={getComponentDrilldownUrl(component.key, 'Reliability', branchName)}>
<BubblesIcon size={14} />
</Link>
<span className="big-spacer-left">
@@ -49,7 +51,7 @@ class BugsAndVulnerabilities extends React.PureComponent {
</span>
<Link
className="button button-small button-compact spacer-left text-text-bottom"
to={getComponentDrilldownUrl(component.key, 'Security')}>
to={getComponentDrilldownUrl(component.key, 'Security', branchName)}>
<BubblesIcon size={14} />
</Link>
</div>

+ 8
- 2
server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js View File

@@ -28,6 +28,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
import { getComponentIssuesUrl } from '../../../helpers/urls';
import CodeSmellIcon from '../../../components/icons-components/CodeSmellIcon';
import { getBranchName } from '../../../helpers/branches';

class CodeSmells extends React.PureComponent {
renderHeader() {
@@ -35,10 +36,15 @@ class CodeSmells extends React.PureComponent {
}

renderDebt(metric, type) {
const { measures, component } = this.props;
const { branch, measures, component } = this.props;
const measure = measures.find(measure => measure.metric.key === metric);
const value = this.props.getValue(measure);
const params = { resolved: 'false', facetMode: 'effort', types: type };
const params = {
branch: getBranchName(branch),
resolved: 'false',
facetMode: 'effort',
types: type
};

if (isDiffMetric(metric)) {
Object.assign(params, { sinceLeakPeriod: 'true' });

+ 10
- 4
server/sonar-web/src/main/js/apps/overview/main/Coverage.js View File

@@ -20,6 +20,7 @@
import React from 'react';
import enhance from './enhance';
import { DrilldownLink } from '../../../components/shared/drilldown-link';
import { getBranchName } from '../../../helpers/branches';
import { getMetricName } from '../helpers/metrics';
import { formatMeasure, getPeriodValue } from '../../../helpers/measures';
import { translate } from '../../../helpers/l10n';
@@ -55,7 +56,7 @@ class Coverage extends React.PureComponent {
}

renderCoverage() {
const { component } = this.props;
const { branch, component } = this.props;
const metric = 'coverage';
const coverage = this.getCoverage();

@@ -67,7 +68,7 @@ class Coverage extends React.PureComponent {

<div className="display-inline-block text-middle">
<div className="overview-domain-measure-value">
<DrilldownLink component={component.key} metric={metric}>
<DrilldownLink branch={getBranchName(branch)} component={component.key} metric={metric}>
<span className="js-overview-main-coverage">
{formatMeasure(coverage, 'PERCENT')}
</span>
@@ -84,7 +85,8 @@ class Coverage extends React.PureComponent {
}

renderNewCoverage() {
const { component, leakPeriod } = this.props;
const { branch, component, leakPeriod } = this.props;
const branchName = getBranchName(branch);
const newCoverageMeasure = this.getNewCoverageMeasure();
const newLinesToCover = this.getNewLinesToCover();

@@ -98,7 +100,10 @@ class Coverage extends React.PureComponent {
const formattedValue =
newCoverageValue != null
? <div>
<DrilldownLink component={component.key} metric={newCoverageMeasure.metric.key}>
<DrilldownLink
branch={branchName}
component={component.key}
metric={newCoverageMeasure.metric.key}>
<span className="js-overview-main-new-coverage">
{formatMeasure(newCoverageValue, 'PERCENT')}
</span>
@@ -111,6 +116,7 @@ class Coverage extends React.PureComponent {
{translate('overview.coverage_on')}
<br />
<DrilldownLink
branch={branchName}
className="spacer-right overview-domain-secondary-measure-value"
component={component.key}
metric={newLinesToCover.metric.key}>

+ 13
- 4
server/sonar-web/src/main/js/apps/overview/main/Duplications.js View File

@@ -20,6 +20,7 @@
import React from 'react';
import enhance from './enhance';
import { DrilldownLink } from '../../../components/shared/drilldown-link';
import { getBranchName } from '../../../helpers/branches';
import { getMetricName } from '../helpers/metrics';
import { formatMeasure, getPeriodValue } from '../../../helpers/measures';
import { translate } from '../../../helpers/l10n';
@@ -39,7 +40,7 @@ class Duplications extends React.PureComponent {
}

renderDuplications() {
const { component, measures } = this.props;
const { branch, component, measures } = this.props;
const measure = measures.find(measure => measure.metric.key === 'duplicated_lines_density');
const duplications = Number(measure.value);

@@ -51,7 +52,10 @@ class Duplications extends React.PureComponent {

<div className="display-inline-block text-middle">
<div className="overview-domain-measure-value">
<DrilldownLink component={component.key} metric="duplicated_lines_density">
<DrilldownLink
branch={getBranchName(branch)}
component={component.key}
metric="duplicated_lines_density">
{formatMeasure(duplications, 'PERCENT')}
</DrilldownLink>
</div>
@@ -66,7 +70,8 @@ class Duplications extends React.PureComponent {
}

renderNewDuplications() {
const { component, measures, leakPeriod } = this.props;
const { branch, component, measures, leakPeriod } = this.props;
const branchName = getBranchName(branch);
const newDuplicationsMeasure = measures.find(
measure => measure.metric.key === 'new_duplicated_lines_density'
);
@@ -82,7 +87,10 @@ class Duplications extends React.PureComponent {
const formattedValue =
newDuplicationsValue != null
? <div>
<DrilldownLink component={component.key} metric={newDuplicationsMeasure.metric.key}>
<DrilldownLink
branch={branchName}
component={component.key}
metric={newDuplicationsMeasure.metric.key}>
<span className="js-overview-main-new-duplications">
{formatMeasure(newDuplicationsValue, 'PERCENT')}
</span>
@@ -95,6 +103,7 @@ class Duplications extends React.PureComponent {
{translate('overview.duplications_on')}
<br />
<DrilldownLink
branch={branchName}
className="spacer-right overview-domain-secondary-measure-value"
component={component.key}
metric={newLinesMeasure.metric.key}>

+ 17
- 7
server/sonar-web/src/main/js/apps/overview/main/enhance.js View File

@@ -26,6 +26,7 @@ import HistoryIcon from '../../../components/icons-components/HistoryIcon';
import Rating from './../../../components/ui/Rating';
import Timeline from '../components/Timeline';
import Tooltip from '../../../components/controls/Tooltip';
import { getBranchName } from '../../../helpers/branches';
import {
formatMeasure,
formatMeasureVariation,
@@ -59,7 +60,7 @@ export default function enhance(ComposedComponent) {
};

renderHeader = (domain, label) => {
const { component } = this.props;
const { branch, component } = this.props;
return (
<div className="overview-card-header">
<div className="overview-title">
@@ -68,7 +69,7 @@ export default function enhance(ComposedComponent) {
</span>
<Link
className="button button-small button-compact spacer-left text-text-bottom"
to={getComponentDrilldownUrl(component.key, domain)}>
to={getComponentDrilldownUrl(component.key, domain, getBranchName(branch))}>
<BubblesIcon size={14} />
</Link>
</div>
@@ -77,7 +78,7 @@ export default function enhance(ComposedComponent) {
};

renderMeasure = metricKey => {
const { measures, component } = this.props;
const { branch, measures, component } = this.props;
const measure = measures.find(measure => measure.metric.key === metricKey);

if (measure == null) {
@@ -87,7 +88,10 @@ export default function enhance(ComposedComponent) {
return (
<div className="overview-domain-measure">
<div className="overview-domain-measure-value">
<DrilldownLink component={component.key} metric={metricKey}>
<DrilldownLink
branch={getBranchName(branch)}
component={component.key}
metric={metricKey}>
<span className="js-overview-main-tests">
{formatMeasure(measure.value, getShortType(measure.metric.type))}
</span>
@@ -125,7 +129,7 @@ export default function enhance(ComposedComponent) {
};

renderRating = metricKey => {
const { component, measures } = this.props;
const { branch, component, measures } = this.props;
const measure = measures.find(measure => measure.metric.key === metricKey);
if (!measure) {
return null;
@@ -136,6 +140,7 @@ export default function enhance(ComposedComponent) {
<Tooltip overlay={title} placement="top">
<div className="overview-domain-measure-sup">
<DrilldownLink
branch={getBranchName(branch)}
className="link-no-underline"
component={component.key}
metric={metricKey}>
@@ -147,10 +152,11 @@ export default function enhance(ComposedComponent) {
};

renderIssues = (metric, type) => {
const { measures, component } = this.props;
const { branch, measures, component } = this.props;
const measure = measures.find(measure => measure.metric.key === metric);
const value = this.getValue(measure);
const params = {
branch: getBranchName(branch),
resolved: 'false',
types: type
};
@@ -182,7 +188,11 @@ export default function enhance(ComposedComponent) {
return (
<Link
className={linkClass}
to={getComponentMeasureHistory(this.props.component.key, metricKey)}>
to={getComponentMeasureHistory(
this.props.component.key,
metricKey,
getBranchName(this.props.branch)
)}>
<HistoryIcon />
</Link>
);

+ 3
- 1
server/sonar-web/src/main/js/apps/overview/meta/Meta.js View File

@@ -31,6 +31,7 @@ import MetaTags from './MetaTags';
import { areThereCustomOrganizations } from '../../../store/rootReducer';

const Meta = ({
branch,
component,
history,
measures,
@@ -58,12 +59,13 @@ const Meta = ({
{description}
</div>}

<MetaSize component={component} measures={measures} />
<MetaSize branch={branch} component={component} measures={measures} />

{isProject && <MetaTags component={component} onComponentChange={onComponentChange} />}

{(isProject || isApplication) &&
<AnalysesList
branch={branch}
project={component.key}
qualifier={component.qualifier}
history={history}

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js View File

@@ -36,7 +36,7 @@ export default class MetaLinks extends React.PureComponent {
}

componentDidUpdate(prevProps) {
if (prevProps.component !== this.props.component) {
if (prevProps.component.key !== this.props.component.key) {
this.loadLinks();
}
}

+ 10
- 2
server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js View File

@@ -23,12 +23,14 @@ import classNames from 'classnames';
import { DrilldownLink } from '../../../components/shared/drilldown-link';
import LanguageDistribution from '../../../components/charts/LanguageDistribution';
import SizeRating from '../../../components/ui/SizeRating';
import { getBranchName } from '../../../helpers/branches';
import { formatMeasure } from '../../../helpers/measures';
import { getMetricName } from '../helpers/metrics';
import { translate } from '../../../helpers/l10n';

export default class MetaSize extends React.PureComponent {
static propTypes = {
branch: PropTypes.object.isRequired,
component: PropTypes.object.isRequired,
measures: PropTypes.array.isRequired
};
@@ -42,7 +44,10 @@ export default class MetaSize extends React.PureComponent {
<span className="spacer-right">
<SizeRating value={ncloc.value} />
</span>
<DrilldownLink component={this.props.component.key} metric="ncloc">
<DrilldownLink
branch={getBranchName(this.props.branch)}
component={this.props.component.key}
metric="ncloc">
{formatMeasure(ncloc.value, 'SHORT_INT')}
</DrilldownLink>
<div className="overview-domain-measure-label text-muted">
@@ -69,7 +74,10 @@ export default class MetaSize extends React.PureComponent {
? <div
id="overview-projects"
className="overview-meta-size-ncloc is-half-width bordered-left">
<DrilldownLink component={this.props.component.key} metric="projects">
<DrilldownLink
branch={getBranchName(this.props.branch)}
component={this.props.component.key}
metric="projects">
{formatMeasure(projects.value, 'SHORT_INT')}
</DrilldownLink>
<div className="overview-domain-measure-label text-muted">

+ 3
- 2
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js View File

@@ -35,12 +35,13 @@ function isProject(component /*: Component */) {

/*::
type Props = {
branch: { name: string },
component: Component,
measures: MeasuresList
};
*/

export default function QualityGate({ component, measures } /*: Props */) {
export default function QualityGate({ branch, component, measures } /*: Props */) {
const statusMeasure = measures.find(measure => measure.metric.key === 'alert_status');
const detailsMeasure = measures.find(measure => measure.metric.key === 'quality_gate_details');

@@ -63,7 +64,7 @@ export default function QualityGate({ component, measures } /*: Props */) {
</h2>

{conditions.length > 0 &&
<QualityGateConditions component={component} conditions={conditions} />}
<QualityGateConditions branch={branch} component={component} conditions={conditions} />}
</div>
);
}

+ 7
- 3
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js View File

@@ -24,6 +24,7 @@ import { Link } from 'react-router';
import { DrilldownLink } from '../../../components/shared/drilldown-link';
import Measure from '../../../components/measure/Measure';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import { getBranchName } from '../../../helpers/branches';
import { getPeriodValue, isDiffMetric, formatMeasure } from '../../../helpers/measures';
import { translate } from '../../../helpers/l10n';
import { getComponentIssuesUrl } from '../../../helpers/urls';
@@ -32,6 +33,7 @@ import { getComponentIssuesUrl } from '../../../helpers/urls';

export default class QualityGateCondition extends React.PureComponent {
/*:: props: {
branch: { name: string },
component: Component,
condition: {
level: string,
@@ -52,16 +54,17 @@ export default class QualityGateCondition extends React.PureComponent {
}
}

getIssuesUrl(sinceLeakPeriod /*: boolean */, customQuery /*: {} */) {
getIssuesUrl = (sinceLeakPeriod /*: boolean */, customQuery /*: {} */) => {
const query /*: Object */ = {
resolved: 'false',
branch: getBranchName(this.props.branch),
...customQuery
};
if (sinceLeakPeriod) {
Object.assign(query, { sinceLeakPeriod: 'true' });
}
return getComponentIssuesUrl(this.props.component.key, query);
}
};

getUrlForCodeSmells(sinceLeakPeriod /*: boolean */) {
return this.getIssuesUrl(sinceLeakPeriod, { types: 'CODE_SMELL' });
@@ -91,7 +94,7 @@ export default class QualityGateCondition extends React.PureComponent {
}

wrapWithLink(children /*: React.Element<*> */) {
const { component, condition } = this.props;
const { branch, component, condition } = this.props;

const className = classNames(
'overview-quality-gate-condition',
@@ -115,6 +118,7 @@ export default class QualityGateCondition extends React.PureComponent {
{children}
</Link>
: <DrilldownLink
branch={getBranchName(branch)}
className={className}
component={component.key}
metric={condition.measure.metric.key}

+ 10
- 3
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js View File

@@ -22,6 +22,7 @@ import { sortBy } from 'lodash';
import QualityGateCondition from './QualityGateCondition';
import { ComponentType, ConditionsListType } from '../propTypes';
import { getMeasuresAndMeta } from '../../../api/measures';
import { getBranchName } from '../../../helpers/branches';
import { enhanceMeasuresWithMetrics } from '../../../helpers/measures';

const LEVEL_ORDER = ['ERROR', 'WARN'];
@@ -35,6 +36,7 @@ function enhanceConditions(conditions, measures) {

export default class QualityGateConditions extends React.PureComponent {
static propTypes = {
// branch
component: ComponentType.isRequired,
conditions: ConditionsListType.isRequired
};
@@ -50,6 +52,7 @@ export default class QualityGateConditions extends React.PureComponent {

componentDidUpdate(prevProps) {
if (
prevProps.branch !== this.props.branch ||
prevProps.conditions !== this.props.conditions ||
prevProps.component !== this.props.component
) {
@@ -62,11 +65,14 @@ export default class QualityGateConditions extends React.PureComponent {
}

loadFailedMeasures() {
const { component, conditions } = this.props;
const { branch, component, conditions } = this.props;
const failedConditions = conditions.filter(c => c.level !== 'OK');
if (failedConditions.length > 0) {
const metrics = failedConditions.map(condition => condition.metric);
getMeasuresAndMeta(component.key, metrics, { additionalFields: 'metrics' }).then(r => {
getMeasuresAndMeta(component.key, metrics, {
additionalFields: 'metrics',
branch: getBranchName(branch)
}).then(r => {
if (this.mounted) {
const measures = enhanceMeasuresWithMetrics(r.component.measures, r.metrics);
this.setState({
@@ -81,7 +87,7 @@ export default class QualityGateConditions extends React.PureComponent {
}

render() {
const { component } = this.props;
const { branch, component } = this.props;
const { loading, conditions } = this.state;

if (loading) {
@@ -101,6 +107,7 @@ export default class QualityGateConditions extends React.PureComponent {
{sortedConditions.map(condition =>
<QualityGateCondition
key={condition.measure.metric.key}
branch={branch}
component={component}
condition={condition}
/>

+ 74
- 9
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js View File

@@ -56,7 +56,13 @@ it('open_issues', () => {
op: 'GT'
};
expect(
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />)
shallow(
<QualityGateCondition
branch={{ isMain: true }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
)
).toMatchSnapshot();
});

@@ -79,28 +85,52 @@ it('new_open_issues', () => {
period: 1
};
expect(
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />)
shallow(
<QualityGateCondition
branch={{ isMain: true }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
)
).toMatchSnapshot();
});

it('reliability_rating', () => {
const condition = mockRatingCondition('reliability_rating');
expect(
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />)
shallow(
<QualityGateCondition
branch={{ isMain: true }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
)
).toMatchSnapshot();
});

it('security_rating', () => {
const condition = mockRatingCondition('security_rating');
expect(
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />)
shallow(
<QualityGateCondition
branch={{ isMain: true }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
)
).toMatchSnapshot();
});

it('sqale_rating', () => {
const condition = mockRatingCondition('sqale_rating');
expect(
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />)
shallow(
<QualityGateCondition
branch={{ isMain: true }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
)
).toMatchSnapshot();
});

@@ -109,7 +139,13 @@ it('new_reliability_rating', () => {
condition.period = 1;
condition.measure.periods = periods;
expect(
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />)
shallow(
<QualityGateCondition
branch={{ isMain: true }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
)
).toMatchSnapshot();
});

@@ -118,7 +154,13 @@ it('new_security_rating', () => {
condition.period = 1;
condition.measure.periods = periods;
expect(
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />)
shallow(
<QualityGateCondition
branch={{ isMain: true }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
)
).toMatchSnapshot();
});

@@ -127,14 +169,24 @@ it('new_maintainability_rating', () => {
condition.period = 1;
condition.measure.periods = periods;
expect(
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />)
shallow(
<QualityGateCondition
branch={{ isMain: true }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
)
).toMatchSnapshot();
});

it('should be able to correctly decide how much decimals to show', () => {
const condition = mockRatingCondition('new_maintainability_rating');
const instance = shallow(
<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />
<QualityGateCondition
branch={{ isMain: true }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
).instance();
expect(instance.getDecimalsNumber(85, 80)).toBe(undefined);
expect(instance.getDecimalsNumber(85, 85)).toBe(undefined);
@@ -144,3 +196,16 @@ it('should be able to correctly decide how much decimals to show', () => {
expect(instance.getDecimalsNumber(85, 85.0000000000000954)).toBe('00000000000009'.length);
expect(instance.getDecimalsNumber(85, 85.00000000000000009)).toBe(undefined);
});

it('should work with branch', () => {
const condition = mockRatingCondition('new_maintainability_rating');
expect(
shallow(
<QualityGateCondition
branch={{ isMain: false, name: 'feature' }}
component={{ key: 'abcd-key' }}
condition={condition}
/>
)
).toMatchSnapshot();
});

+ 1
- 0
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap View File

@@ -9,6 +9,7 @@ exports[`renders 1`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}

+ 67
- 0
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap View File

@@ -9,6 +9,7 @@ exports[`new_maintainability_rating 1`] = `
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "abcd-key",
"resolved": "false",
"sinceLeakPeriod": "true",
@@ -131,6 +132,7 @@ exports[`new_reliability_rating 1`] = `
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "abcd-key",
"resolved": "false",
"severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
@@ -198,6 +200,7 @@ exports[`new_security_rating 1`] = `
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "abcd-key",
"resolved": "false",
"severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
@@ -315,6 +318,7 @@ exports[`reliability_rating 1`] = `
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "abcd-key",
"resolved": "false",
"severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
@@ -375,6 +379,7 @@ exports[`security_rating 1`] = `
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "abcd-key",
"resolved": "false",
"severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
@@ -426,6 +431,67 @@ exports[`security_rating 1`] = `
</Link>
`;

exports[`should work with branch 1`] = `
<Link
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"branch": "feature",
"id": "abcd-key",
"resolved": "false",
"sinceLeakPeriod": "true",
"types": "CODE_SMELL",
},
}
}
>
<div
className="overview-quality-gate-condition-container"
>
<div
className="overview-quality-gate-condition-value"
>
<Measure
decimals={null}
measure={
Object {
"leak": "3",
"metric": Object {
"key": "new_maintainability_rating",
"name": "new_maintainability_rating",
"type": "RATING",
},
"value": "3",
}
}
/>
</div>
<div>
<div
className="overview-quality-gate-condition-metric"
>
<IssueTypeIcon
className="little-spacer-right"
query="new_maintainability_rating"
/>
new_maintainability_rating
</div>
<div
className="overview-quality-gate-threshold"
>
quality_gates.operator.GT.rating

A
</div>
</div>
</div>
</Link>
`;

exports[`sqale_rating 1`] = `
<Link
className="overview-quality-gate-condition overview-quality-gate-condition-error"
@@ -435,6 +501,7 @@ exports[`sqale_rating 1`] = `
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "abcd-key",
"resolved": "false",
"types": "CODE_SMELL",

+ 8
- 3
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js View File

@@ -26,6 +26,7 @@ import { getAllTimeMachineData } from '../../../api/time-machine';
import { getMetrics } from '../../../api/metrics';
import * as api from '../../../api/projectActivity';
import * as actions from '../actions';
import { getBranchName } from '../../../helpers/branches';
import { parseDate } from '../../../helpers/dates';
import { getCustomGraph, getGraph } from '../../../helpers/storage';
import {
@@ -42,6 +43,7 @@ import {

/*::
type Props = {
branch: {},
location: { pathname: string, query: RawQuery },
component: {
configuration?: { showHistory: boolean },
@@ -93,7 +95,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
}
this.context.router.replace({
pathname: props.location.pathname,
query: serializeUrlQuery(newQuery)
query: { ...serializeUrlQuery(newQuery), branch: getBranchName(props.branch) }
});
}
}
@@ -167,7 +169,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
[string]: string
} */
) => {
const parameters = { project, p, ps };
const parameters = { project, p, ps, branch: getBranchName(this.props.branch) };
return api
.getProjectActivity({ ...parameters, ...additional })
.then(({ analyses, paging }) => ({
@@ -180,7 +182,9 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
if (metrics.length <= 0) {
return Promise.resolve([]);
}
return getAllTimeMachineData(this.props.component.key, metrics).then(
return getAllTimeMachineData(this.props.component.key, metrics, {
branch: getBranchName(this.props.branch)
}).then(
({ measures }) =>
measures.map(measure => ({
metric: measure.metric,
@@ -281,6 +285,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...query,
branch: getBranchName(this.props.branch),
id: this.props.component.key
}
});

+ 1
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js View File

@@ -63,6 +63,7 @@ const DEFAULT_PROPS = {
addVersion: () => {},
analyses: ANALYSES,
analysesLoading: false,
branch: { isMain: true },
changeEvent: () => {},
deleteAnalysis: () => {},
deleteEvent: () => {},

+ 22
- 7
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js View File

@@ -419,7 +419,7 @@ export default class SourceViewerBase extends React.PureComponent {
};

loadDuplications = (line /*: SourceLine */) => {
getDuplications(this.props.component).then(r => {
getDuplications(this.props.component, this.props.branch).then(r => {
if (this.mounted) {
this.setState(
{
@@ -440,13 +440,22 @@ export default class SourceViewerBase extends React.PureComponent {
};

showMeasures = () => {
const measuresOverlay = new MeasuresOverlay({ component: this.state.component, large: true });
const measuresOverlay = new MeasuresOverlay({
branch: this.props.branch,
component: this.state.component,
large: true
});
measuresOverlay.render();
};

handleCoverageClick = (line /*: SourceLine */, element /*: HTMLElement */) => {
getTests(this.props.component, line.line).then(tests => {
const popup = new CoveragePopupView({ line, tests, triggerEl: element });
getTests(this.props.component, line.line, this.props.branch).then(tests => {
const popup = new CoveragePopupView({
line,
tests,
triggerEl: element,
branch: this.props.branch
});
popup.render();
});
};
@@ -477,7 +486,8 @@ export default class SourceViewerBase extends React.PureComponent {
inRemovedComponent,
component: this.state.component,
files: this.state.duplicatedFiles,
triggerEl: element
triggerEl: element,
branch: this.props.branch
});
popup.render();
}
@@ -500,7 +510,8 @@ export default class SourceViewerBase extends React.PureComponent {
const popup = new LineActionsPopupView({
line,
triggerEl: element,
component: this.state.component
component: this.state.component,
branch: this.props.branch
});
popup.render();
}
@@ -637,7 +648,11 @@ export default class SourceViewerBase extends React.PureComponent {

return (
<div className={className} ref={node => (this.node = node)}>
<SourceViewerHeader component={this.state.component} showMeasures={this.showMeasures} />
<SourceViewerHeader
branch={this.props.branch}
component={this.state.component}
showMeasures={this.showMeasures}
/>
{notAccessible &&
<div className="alert alert-warning spacer-top">
{translate('code_viewer.no_source_code_displayed_due_to_security')}

+ 17
- 6
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js View File

@@ -29,6 +29,7 @@ import { formatMeasure } from '../../helpers/measures';

export default class SourceViewerHeader extends React.PureComponent {
/*:: props: {
branch?: string,
component: {
canMarkAsFavorite: boolean,
key: string,
@@ -60,7 +61,7 @@ export default class SourceViewerHeader extends React.PureComponent {
e.preventDefault();
const { key } = this.props.component;
const Workspace = require('../workspace/main').default;
Workspace.openComponent({ key });
Workspace.openComponent({ key, branch: this.props.branch });
};

render() {
@@ -78,8 +79,11 @@ export default class SourceViewerHeader extends React.PureComponent {
const isUnitTest = q === 'UTS';
// TODO check if source viewer is displayed inside workspace
const workspace = false;
const rawSourcesLink =
let rawSourcesLink =
window.baseUrl + `/api/sources/raw?key=${encodeURIComponent(this.props.component.key)}`;
if (this.props.branch) {
rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`;
}

// TODO favorite
return (
@@ -87,14 +91,14 @@ export default class SourceViewerHeader extends React.PureComponent {
<div className="source-viewer-header-component">
<div className="component-name">
<div className="component-name-parent">
<Link to={getProjectUrl(project)} className="link-with-icon">
<Link to={getProjectUrl(project, this.props.branch)} className="link-with-icon">
<QualifierIcon qualifier="TRK" /> <span>{projectName}</span>
</Link>
</div>

{subProject != null &&
<div className="component-name-parent">
<Link to={getProjectUrl(subProject)} className="link-with-icon">
<Link to={getProjectUrl(subProject, this.props.branch)} className="link-with-icon">
<QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span>
</Link>
</div>}
@@ -124,7 +128,10 @@ export default class SourceViewerHeader extends React.PureComponent {
<Link
className="js-new-window"
target="_blank"
to={{ pathname: '/component', query: { id: this.props.component.key } }}>
to={{
pathname: '/component',
query: { branch: this.props.branch, id: this.props.component.key }
}}>
{translate('component_viewer.new_window')}
</Link>
</li>
@@ -166,7 +173,11 @@ export default class SourceViewerHeader extends React.PureComponent {
<div className="source-viewer-header-measure">
<span className="source-viewer-header-measure-value">
<Link
to={getComponentIssuesUrl(project, { resolved: 'false', fileUuids: uuid })}
to={getComponentIssuesUrl(project, {
resolved: 'false',
fileUuids: uuid,
branch: this.props.branch
})}
className="source-viewer-header-external-link"
target="_blank">
{measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}{' '}

+ 1
- 1
server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js View File

@@ -38,7 +38,7 @@ export default Popup.extend({
e.stopPropagation();
const key = $(e.currentTarget).data('key');
const Workspace = require('../../workspace/main').default;
Workspace.openComponent({ key });
Workspace.openComponent({ key, branch: this.options.branch });
},

serializeData() {

+ 1
- 1
server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js View File

@@ -34,7 +34,7 @@ export default Popup.extend({
const key = $(e.currentTarget).data('key');
const line = $(e.currentTarget).data('line');
const Workspace = require('../../workspace/main').default;
Workspace.openComponent({ key, line });
Workspace.openComponent({ key, line, branch: this.options.branch });
},

serializeData() {

+ 7
- 4
server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js View File

@@ -24,9 +24,12 @@ export default Popup.extend({
template: Template,

serializeData() {
const { component, line } = this.options;
return {
permalink: window.baseUrl + `/component?id=${encodeURIComponent(component.key)}&line=${line}`
};
const { component, line, branch } = this.options;
let permalink =
window.baseUrl + `/component?id=${encodeURIComponent(component.key)}&line=${line}`;
if (branch) {
permalink += `&branch=${encodeURIComponent(branch)}`;
}
return { permalink };
}
});

+ 7
- 2
server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js View File

@@ -141,7 +141,11 @@ export default ModalView.extend({
.filter(metric => metric.type !== 'DATA' && !metric.hidden)
.map(metric => metric.key);

return getMeasures(this.options.component.key, metricsToRequest).then(measures => {
return getMeasures(
this.options.component.key,
metricsToRequest,
this.options.branch
).then(measures => {
let nextMeasures = this.options.component.measures || {};
measures.forEach(measure => {
const metric = metrics.find(metric => metric.key === measure.metric);
@@ -160,6 +164,7 @@ export default ModalView.extend({
return new Promise(resolve => {
const url = window.baseUrl + '/api/issues/search';
const options = {
branch: this.options.branch,
componentKeys: this.options.component.key,
resolved: false,
ps: 1,
@@ -191,7 +196,7 @@ export default ModalView.extend({
requestTests() {
return new Promise(resolve => {
const url = window.baseUrl + '/api/tests/list';
const options = { testFileKey: this.options.component.key };
const options = { branch: this.options.branch, testFileKey: this.options.component.key };

$.get(url, options).done(data => {
this.tests = data.tests;

+ 10
- 17
server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx View File

@@ -18,28 +18,21 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import ShortLivingBranchIcon from './ShortLivingBranchIcon';
import LongLivingBranchIcon from './LongLivingBranchIcon';
// import PullRequestIcon from './PullRequestIcon';
import { Branch } from '../../app/types';
import { isShortLivingBranch } from '../../helpers/branches';

interface Props {
branch: Branch;
className?: string;
color?: string;
size?: number;
}

export default function BranchIcon({ className, color = '#4b9fd5', size = 14 }: Props) {
/* eslint-disable max-len */
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
height={size}
width={size}
viewBox="0 0 16 16">
<g transform="matrix(0.0416667,0,0,0.0416667,2.98284,-1.32102)">
<path
d="M72,368C72,361.333 69.667,355.667 65,351C60.333,346.333 54.667,344 48,344C41.333,344 35.667,346.333 31,351C26.333,355.667 24,361.333 24,368C24,374.667 26.333,380.333 31,385C35.667,389.667 41.333,392 48,392C54.667,392 60.333,389.667 65,385C69.667,380.333 72,374.667 72,368ZM72,80C72,73.333 69.667,67.667 65,63C60.333,58.333 54.667,56 48,56C41.333,56 35.667,58.333 31,63C26.333,67.667 24,73.333 24,80C24,86.667 26.333,92.333 31,97C35.667,101.667 41.333,104 48,104C54.667,104 60.333,101.667 65,97C69.667,92.333 72,86.667 72,80ZM232,112C232,105.333 229.667,99.667 225,95C220.333,90.333 214.667,88 208,88C201.333,88 195.667,90.333 191,95C186.333,99.667 184,105.333 184,112C184,118.667 186.333,124.333 191,129C195.667,133.667 201.333,136 208,136C214.667,136 220.333,133.667 225,129C229.667,124.333 232,118.667 232,112ZM256,112C256,120.667 253.833,128.708 249.5,136.125C245.167,143.542 239.333,149.333 232,153.5C231.667,201.333 212.833,235.833 175.5,257C164.167,263.333 147.25,270.083 124.75,277.25C103.417,283.917 89.292,289.833 82.375,295C75.458,300.167 72,308.5 72,320L72,326.5C79.333,330.667 85.167,336.458 89.5,343.875C93.833,351.292 96,359.333 96,368C96,381.333 91.333,392.667 82,402C72.667,411.333 61.333,416 48,416C34.667,416 23.333,411.333 14,402C4.667,392.667 0,381.333 0,368C0,359.333 2.167,351.292 6.5,343.875C10.833,336.458 16.667,330.667 24,326.5L24,121.5C16.667,117.333 10.833,111.542 6.5,104.125C2.167,96.708 0,88.667 0,80C0,66.667 4.667,55.333 14,46C23.333,36.667 34.667,32 48,32C61.333,32 72.667,36.667 82,46C91.333,55.333 96,66.667 96,80C96,88.667 93.833,96.708 89.5,104.125C85.167,111.542 79.333,117.333 72,121.5L72,245.75C81,241.417 93.833,236.667 110.5,231.5C119.667,228.667 126.958,226.208 132.375,224.125C137.792,222.042 143.667,219.458 150,216.375C156.333,213.292 161.25,210 164.75,206.5C168.25,203 171.625,198.75 174.875,193.75C178.125,188.75 180.458,182.958 181.875,176.375C183.292,169.792 184,162.167 184,153.5C176.667,149.333 170.833,143.542 166.5,136.125C162.167,128.708 160,120.667 160,112C160,98.667 164.667,87.333 174,78C183.333,68.667 194.667,64 208,64C221.333,64 232.667,68.667 242,78C251.333,87.333 256,98.667 256,112Z"
style={{ fill: color, fillRule: 'nonzero' }}
/>
</g>
</svg>
);
export default function BranchIcon({ branch, ...props }: Props) {
return isShortLivingBranch(branch)
? <ShortLivingBranchIcon {...props} />
: <LongLivingBranchIcon {...props} />;
}

+ 45
- 0
server/sonar-web/src/main/js/components/icons-components/LongLivingBranchIcon.tsx View File

@@ -0,0 +1,45 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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';

interface Props {
className?: string;
color?: string;
size?: number;
}

export default function LongLivingBranchIcon({ className, color = '#4b9fd5', size = 16 }: Props) {
/* eslint-disable max-len */
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
height={size}
width={size}
viewBox="0 0 16 16">
<g transform="translate(5, 0)">
<path
style={{ fill: color }}
d="M4.5 8c0-.9-.6-1.7-1.5-1.9V4c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2C.5 3 1.1 3.8 2 4v2.1C1.1 6.3.5 7.1.5 8s.6 1.7 1.5 2v2.1c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.7-1.5-1.9V10c.9-.3 1.5-1 1.5-2zm-3-5.9c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.5-1-1zm0 5.9c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.4-1-1zm2 6c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .5 1 1z"
/>
</g>
</svg>
);
}

+ 43
- 0
server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx View File

@@ -0,0 +1,43 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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';

interface Props {
className?: string;
color?: string;
size?: number;
}

export default function PullRequestIcon({ className, color = '#4b9fd5', size = 16 }: Props) {
/* eslint-disable max-len */
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
height={size}
width={size}
viewBox="0 0 16 16">
<path
style={{ fill: color }}
d="M3 11.9V4.1c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2c0 .9.6 1.7 1.5 1.9v7.8c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.6-1.5-1.9zM1.5 2.2c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.4-1-1zm1 12.7c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zM14 11.9V5.5c0-.1-.2-3.1-5.1-3.5L10.1.8 9.5.1 6.9 2.6l2.6 2.5.7-.7L8.8 3c4 .2 4.2 2.4 4.2 2.5v6.4c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.6-1.5-1.9zm-.5 3c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1z"
/>
</svg>
);
}

+ 45
- 0
server/sonar-web/src/main/js/components/icons-components/ShortLivingBranchIcon.tsx View File

@@ -0,0 +1,45 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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';

interface Props {
className?: string;
color?: string;
size?: number;
}

export default function ShortLivingBranchIcon({ className, color = '#4b9fd5', size = 16 }: Props) {
/* eslint-disable max-len */
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
height={size}
width={size}
viewBox="0 0 16 16">
<g transform="translate(3, 0)">
<path
style={{ fill: color }}
d="M9.5 6.5c0-1.1-.9-2-2-2s-2 .9-2 2c0 .8.5 1.5 1.2 1.8-.3.6-.7 1.1-1.2 1.4-.9.5-1.9.5-2.5.4V4c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2C.5 3 1.1 3.8 2 4v8c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.7-1.5-1.9v-1c.2 0 .5.1.7.1.7 0 1.5-.1 2.2-.6.8-.5 1.4-1.2 1.7-2.1 1.1 0 1.9-.9 1.9-1.9zm-8-4.4c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.5-1-1zm2 11.9c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .4 1 1zm4-6.5c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1z"
/>
</g>
</svg>
);
}

+ 10
- 2
server/sonar-web/src/main/js/components/shared/drilldown-link.js View File

@@ -49,6 +49,7 @@ const ISSUE_MEASURES = [

export class DrilldownLink extends React.PureComponent {
static propTypes = {
branch: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
className: PropTypes.string,
component: PropTypes.string.isRequired,
@@ -118,7 +119,10 @@ export class DrilldownLink extends React.PureComponent {
};

renderIssuesLink = () => {
const url = getComponentIssuesUrl(this.props.component, this.propsToIssueParams());
const url = getComponentIssuesUrl(this.props.component, {
...this.propsToIssueParams(),
branch: this.props.branch
});

return (
<Link to={url} className={this.props.className}>
@@ -132,7 +136,11 @@ export class DrilldownLink extends React.PureComponent {
return this.renderIssuesLink();
}

const url = getComponentDrilldownUrl(this.props.component, this.props.metric);
const url = getComponentDrilldownUrl(
this.props.component,
this.props.metric,
this.props.branch
);
return (
<Link to={url} className={this.props.className}>
{this.props.children}

+ 71
- 0
server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts View File

@@ -0,0 +1,71 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { sortBranchesAsTree } from '../branches';
import { MainBranch, BranchType, ShortLivingBranch, LongLivingBranch } from '../../app/types';

describe('#sortBranchesAsTree', () => {
it('sorts main branch and short-living branches', () => {
const main = mainBranch();
const foo = shortLivingBranch('foo', 'master');
const bar = shortLivingBranch('bar', 'master');
expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]);
});

it('sorts main branch and long-living branches', () => {
const main = mainBranch();
const foo = longLivingBranch('foo');
const bar = longLivingBranch('bar');
expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]);
});

it('sorts all types of branches', () => {
const main = mainBranch();
const shortFoo = shortLivingBranch('shortFoo', 'master');
const shortBar = shortLivingBranch('shortBar', 'longBaz');
const shortPre = shortLivingBranch('shortPre', 'shortFoo');
const longBaz = longLivingBranch('longBaz');
const longQux = longLivingBranch('longQux');
const longQwe = longLivingBranch('longQwe');
// - main - main
// - shortFoo - shortFoo
// - shortPre - shortPre
// - longBaz ----> - longBaz
// - shortBar - shortBar
// - longQwe - longQwe
// - longQux - longQux
expect(
sortBranchesAsTree([main, shortFoo, shortBar, shortPre, longBaz, longQux, longQwe])
).toEqual([main, shortFoo, shortPre, longBaz, shortBar, longQux, longQwe]);
});
});

function mainBranch(): MainBranch {
return { isMain: true, name: 'master' };
}

function shortLivingBranch(name: string, mergeBranch: string): ShortLivingBranch {
const status = { bugs: 0, codeSmells: 0, vulnerabilities: 0 };
return { isMain: false, mergeBranch, name, status, type: BranchType.SHORT };
}

function longLivingBranch(name: string): LongLivingBranch {
const status = { qualityGateStatus: 'OK' };
return { isMain: false, name, status, type: BranchType.LONG };
}

+ 46
- 11
server/sonar-web/src/main/js/helpers/branches.ts View File

@@ -17,20 +17,55 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Branch, BranchType, ShortLivingBranch } from '../app/types';
import { sortBy } from 'lodash';
import { Branch, BranchType, ShortLivingBranch, LongLivingBranch } from '../app/types';

export const MAIN_BRANCH: Branch = {
isMain: true,
name: undefined,
type: BranchType.LONG
};
export function isShortLivingBranch(branch: Branch | null): branch is ShortLivingBranch {
return branch != null && !branch.isMain && branch.type === BranchType.SHORT;
}

const MAIN_BRANCH_DISPLAY_NAME = 'master';
export function isLongLivingBranch(branch: Branch | null): branch is LongLivingBranch {
return branch != null && !branch.isMain && branch.type === BranchType.LONG;
}

export function isShortLivingBranch(branch: Branch | null): branch is ShortLivingBranch {
return branch != null && branch.type === BranchType.SHORT;
export function getBranchName(branch: Branch): string | undefined {
return branch.isMain ? undefined : branch.name;
}

export function getBranchDisplayName(branch: Branch): string {
return branch.isMain ? MAIN_BRANCH_DISPLAY_NAME : branch.name;
export function sortBranchesAsTree(branches: Branch[]): Branch[] {
const result: Branch[] = [];

const shortLivingBranches = branches.filter(isShortLivingBranch);

// main branch is always first
const mainBranch = branches.find(branch => branch.isMain);
if (mainBranch) {
result.push(mainBranch, ...getNestedShortLivingBranches(mainBranch.name));
}

// the all long-living branches
sortBy(branches.filter(isLongLivingBranch), 'name').forEach(longLivingBranch => {
result.push(longLivingBranch, ...getNestedShortLivingBranches(longLivingBranch.name));
});

// finally all orhpan branches
result.push(...shortLivingBranches.filter(branch => branch.isOrphan));

return result;

/** Get all short-living branches (possibly nested) which should be merged to a given branch */
function getNestedShortLivingBranches(mergeBranch: string): ShortLivingBranch[] {
const found: ShortLivingBranch[] = shortLivingBranches.filter(
branch => branch.mergeBranch === mergeBranch
);

let i = 0;
while (i < found.length) {
const current = found[i];
found.push(...shortLivingBranches.filter(branch => branch.mergeBranch === current.name));
i++;
}

return sortBy(found, 'name');
}
}

+ 6
- 4
server/sonar-web/src/main/js/helpers/testUtils.ts View File

@@ -63,12 +63,14 @@ export function elementKeydown(element: ShallowWrapper, keyCode: number): void {
});
}

export function doAsync(fn: Function): Promise<void> {
export function doAsync(fn?: Function): Promise<void> {
return new Promise(resolve => {
setTimeout(() => {
fn();
setImmediate(() => {
if (fn) {
fn();
}
resolve();
}, 0);
});
});
}


+ 18
- 9
server/sonar-web/src/main/js/helpers/urls.ts View File

@@ -23,7 +23,7 @@ import { getProfilePath } from '../apps/quality-profiles/utils';
import { Branch } from '../app/types';

interface Query {
[x: string]: string;
[x: string]: string | undefined;
}

interface Location {
@@ -34,12 +34,15 @@ interface Location {
/**
* Generate URL for a component's home page
*/
export function getComponentUrl(componentKey: string): string {
return (window as any).baseUrl + '/dashboard?id=' + encodeURIComponent(componentKey);
export function getComponentUrl(componentKey: string, branch?: string): string {
const branchQuery = branch ? `&branch=${encodeURIComponent(branch)}` : '';
return (
(window as any).baseUrl + '/dashboard?id=' + encodeURIComponent(componentKey) + branchQuery
);
}

export function getProjectUrl(key: string): Location {
return { pathname: '/dashboard', query: { id: key } };
export function getProjectUrl(key: string, branch?: string): Location {
return { pathname: '/dashboard', query: { id: key, branch } };
}

export function getProjectBranchUrl(key: string, branch: Branch) {
@@ -48,6 +51,8 @@ export function getProjectBranchUrl(key: string, branch: Branch) {
pathname: '/project/issues',
query: { branch: branch.name, id: key, resolved: 'false' }
};
} else if (!branch.isMain) {
return { pathname: '/dashboard', query: { branch: branch.name, id: key } };
} else {
return { pathname: '/dashboard', query: { id: key } };
}
@@ -75,17 +80,21 @@ export function getComponentIssuesUrlAsString(componentKey: string, query?: Quer
/**
* Generate URL for a component's drilldown page
*/
export function getComponentDrilldownUrl(componentKey: string, metric: string): Location {
return { pathname: '/component_measures', query: { id: componentKey, metric } };
export function getComponentDrilldownUrl(componentKey: string, metric: string, branch?: string) {
return { pathname: '/component_measures', query: { id: componentKey, metric, branch } };
}

/**
* Generate URL for a component's measure history
*/
export function getComponentMeasureHistory(componentKey: string, metric: string): Location {
export function getComponentMeasureHistory(
componentKey: string,
metric: string,
branch?: string
): Location {
return {
pathname: '/project/activity',
query: { id: componentKey, graph: 'custom', custom_metrics: metric }
query: { id: componentKey, graph: 'custom', custom_metrics: metric, branch }
};
}


+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -988,7 +988,8 @@ dependencies.not_used=Not used
#------------------------------------------------------------------------------

dashboard.no_dashboard=No dashboard
dashboard.project_not_found=The requested project does not exist. Either it has never been analyzed successfully or it has been deleted.
dashboard.project_not_found=The requested project does not exist.
dashboard.project_not_found.2=Either it has never been analyzed successfully or it has been deleted.


#------------------------------------------------------------------------------

Loading…
Cancel
Save