Browse Source

SONAR-10813 Add project branches

tags/7.5
Pascal Mugnier 6 years ago
parent
commit
e5827b3671
34 changed files with 220 additions and 63 deletions
  1. 8
    2
      server/sonar-web/src/main/js/api/application.ts
  2. 1
    0
      server/sonar-web/src/main/js/api/quality-gates.ts
  3. 56
    18
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
  4. 2
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  5. 2
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
  6. 7
    7
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
  7. 1
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
  8. 2
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap
  9. 6
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  10. 8
    0
      server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap
  11. 3
    1
      server/sonar-web/src/main/js/app/types.ts
  12. 1
    0
      server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap
  13. 1
    0
      server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap
  14. 1
    1
      server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
  15. 22
    3
      server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
  16. 1
    0
      server/sonar-web/src/main/js/apps/code/types.ts
  17. 25
    1
      server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js
  18. 2
    12
      server/sonar-web/src/main/js/apps/component/components/App.tsx
  19. 3
    2
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  20. 3
    0
      server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap
  21. 8
    3
      server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.tsx
  22. 9
    2
      server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
  23. 5
    1
      server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.tsx
  24. 6
    3
      server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx
  25. 4
    2
      server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx
  26. 1
    0
      server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap
  27. 3
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
  28. 1
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap
  29. 1
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap
  30. 1
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
  31. 1
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
  32. 18
    0
      server/sonar-web/src/main/js/helpers/branches.ts
  33. 5
    3
      server/sonar-web/src/main/js/helpers/urls.ts
  34. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 8
- 2
server/sonar-web/src/main/js/api/application.ts View File

projectName: string; projectName: string;
} }


export function getApplicationLeak(application: string): Promise<Array<ApplicationLeak>> {
return getJSON('/api/applications/show_leak', { application }).then(r => r.leaks, throwGlobalError);
export function getApplicationLeak(
application: string,
branch?: string
): Promise<Array<ApplicationLeak>> {
return getJSON('/api/applications/show_leak', { application, branch }).then(
r => r.leaks,
throwGlobalError
);
} }

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



export function getApplicationQualityGate(data: { export function getApplicationQualityGate(data: {
application: string; application: string;
branch?: string;
organization?: string; organization?: string;
}): Promise<ApplicationQualityGate> { }): Promise<ApplicationQualityGate> {
return getJSON('/api/qualitygates/application_status', data).catch(throwGlobalError); return getJSON('/api/qualitygates/application_status', data).catch(throwGlobalError);

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

import * as React from 'react'; import * as React from 'react';
import * as PropTypes from 'prop-types'; import * as PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; import ComponentNavBranchesMenu from './ComponentNavBranchesMenu';
import DocTooltip from '../../../../components/docs/DocTooltip'; import DocTooltip from '../../../../components/docs/DocTooltip';
import { BranchLike, Component } from '../../../types'; import { BranchLike, Component } from '../../../types';
mounted = false; mounted = false;


static contextTypes = { static contextTypes = {
branchesEnabled: PropTypes.bool.isRequired
branchesEnabled: PropTypes.bool.isRequired,
canAdmin: PropTypes.bool.isRequired
}; };


state: State = { state: State = {
} }
}; };


renderOverlay = () => {
const adminLink = {
pathname: '/project/admin/extension/governance/console',
query: { id: this.props.component.breadcrumbs[0].key, qualifier: 'APP' }
};
return (
<>
<p>{translate('application.branches.help')}</p>
<hr className="spacer-top spacer-bottom" />
<Link className="spacer-left link-no-underline" to={adminLink}>
{translate('application.branches.link')}
</Link>
</>
);
};

render() { render() {
const { branchLikes, currentBranchLike } = this.props; const { branchLikes, currentBranchLike } = this.props;
const { configuration } = this.props.component;
const { configuration, breadcrumbs } = this.props.component;


if (isSonarCloud() && !this.context.branchesEnabled) { if (isSonarCloud() && !this.context.branchesEnabled) {
return null; return null;
} }


const displayName = getBranchLikeDisplayName(currentBranchLike); const displayName = getBranchLikeDisplayName(currentBranchLike);
const isApp = breadcrumbs && breadcrumbs[0] && breadcrumbs[0].qualifier === 'APP';


if (!this.context.branchesEnabled) {
if (isApp && branchLikes.length < 2) {
return ( return (
<div className="navbar-context-branches"> <div className="navbar-context-branches">
<BranchIcon <BranchIcon
fill={theme.gray80} fill={theme.gray80}
/> />
<span className="note">{displayName}</span> <span className="note">{displayName}</span>
<DocTooltip className="spacer-left" doc="branches/no-branch-support">
<PlusCircleIcon fill={theme.gray71} size={12} />
</DocTooltip>
</div>
);
}

if (branchLikes.length < 2) {
return (
<div className="navbar-context-branches">
<BranchIcon branchLike={currentBranchLike} className="little-spacer-right" />
<span className="note">{displayName}</span>
<DocTooltip className="spacer-left" doc="branches/single-branch">
<PlusCircleIcon fill={theme.blue} size={12} />
</DocTooltip>
{configuration &&
configuration.showSettings && (
<HelpTooltip className="spacer-left" overlay={this.renderOverlay()}>
<PlusCircleIcon className="vertical-middle" fill={theme.blue} size={12} />
</HelpTooltip>
)}
</div> </div>
); );
} else {
if (!this.context.branchesEnabled) {
return (
<div className="navbar-context-branches">
<BranchIcon
branchLike={currentBranchLike}
className="little-spacer-right"
fill={theme.gray80}
/>
<span className="note">{displayName}</span>
<DocTooltip className="spacer-left" doc="branches/no-branch-support">
<PlusCircleIcon fill={theme.gray71} size={12} />
</DocTooltip>
</div>
);
}

if (branchLikes.length < 2) {
return (
<div className="navbar-context-branches">
<BranchIcon branchLike={currentBranchLike} className="little-spacer-right" />
<span className="note">{displayName}</span>
<DocTooltip className="spacer-left" doc="branches/single-branch">
<PlusCircleIcon fill={theme.blue} size={12} />
</DocTooltip>
</div>
);
}
} }


return ( return (

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



renderExtension = ({ key, name }: Extension, isAdmin: boolean) => { renderExtension = ({ key, name }: Extension, isAdmin: boolean) => {
const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`; const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`;
const query = { id: this.props.component.key, qualifier: this.props.component.qualifier };
return ( return (
<li key={key}> <li key={key}>
<Link to={{ pathname, query: { id: this.props.component.key } }} activeClassName="active">
<Link activeClassName="active" to={{ pathname, query }}>
{name} {name}
</Link> </Link>
</li> </li>

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

if (component.qualifier === 'VW' || component.qualifier === 'SVW') { if (component.qualifier === 'VW' || component.qualifier === 'SVW') {
currentPage = { type: HomePageType.Portfolio, component: component.key }; currentPage = { type: HomePageType.Portfolio, component: component.key };
} else if (component.qualifier === 'APP') { } else if (component.qualifier === 'APP') {
currentPage = { type: HomePageType.Application, component: component.key };
const branch = isLongLivingBranch(branchLike) ? branchLike.name : undefined;
currentPage = { type: HomePageType.Application, component: component.key, branch };
} else if (component.qualifier === 'TRK') { } else if (component.qualifier === 'TRK') {
// when home page is set to the default branch of a project, its name is returned as `undefined` // when home page is set to the default branch of a project, its name is returned as `undefined`
const branch = isLongLivingBranch(branchLike) ? branchLike.name : undefined; const branch = isLongLivingBranch(branchLike) ? branchLike.name : undefined;

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

component={component} component={component}
currentBranchLike={mainBranch} currentBranchLike={mainBranch}
/>, />,
{ context: { branchesEnabled: true } }
{ context: { branchesEnabled: true, canAdmin: true } }
) )
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
component={component} component={component}
currentBranchLike={branch} currentBranchLike={branch}
/>, />,
{ context: { branchesEnabled: true } }
{ context: { branchesEnabled: true, canAdmin: true } }
) )
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
component={component} component={component}
currentBranchLike={pullRequest} currentBranchLike={pullRequest}
/>, />,
{ context: { branchesEnabled: true } }
{ context: { branchesEnabled: true, canAdmin: true } }
) )
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
component={component} component={component}
currentBranchLike={mainBranch} currentBranchLike={mainBranch}
/>, />,
{ context: { branchesEnabled: true } }
{ context: { branchesEnabled: true, canAdmin: true } }
); );
expect(wrapper.find('Toggler').prop('open')).toBe(false); expect(wrapper.find('Toggler').prop('open')).toBe(false);
click(wrapper.find('a')); click(wrapper.find('a'));
component={component} component={component}
currentBranchLike={mainBranch} currentBranchLike={mainBranch}
/>, />,
{ context: { branchesEnabled: true } }
{ context: { branchesEnabled: true, canAdmin: true } }
); );
expect(wrapper.find('DocTooltip')).toMatchSnapshot(); expect(wrapper.find('DocTooltip')).toMatchSnapshot();
}); });
component={component} component={component}
currentBranchLike={mainBranch} currentBranchLike={mainBranch}
/>, />,
{ context: { branchesEnabled: false } }
{ context: { branchesEnabled: false, canAdmin: true } }
); );
expect(wrapper.find('DocTooltip')).toMatchSnapshot(); expect(wrapper.find('DocTooltip')).toMatchSnapshot();
}); });
component={component} component={component}
currentBranchLike={mainBranch} currentBranchLike={mainBranch}
/>, />,
{ context: { branchesEnabled: false, onSonarCloud: true } }
{ context: { branchesEnabled: false, onSonarCloud: true, canAdmin: true } }
); );
expect(wrapper.type()).toBeNull(); expect(wrapper.type()).toBeNull();
}); });

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

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "component", "id": "component",
}, },
} }

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

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "my-project", "id": "my-project",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "my-project", "id": "my-project",
}, },
} }

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

"pathname": "/project/extension/component-foo", "pathname": "/project/extension/component-foo",
"query": Object { "query": Object {
"id": "foo", "id": "foo",
"qualifier": "TRK",
}, },
} }
} }
"pathname": "/project/admin/extension/foo", "pathname": "/project/admin/extension/foo",
"query": Object { "query": Object {
"id": "foo", "id": "foo",
"qualifier": "TRK",
}, },
} }
} }
"pathname": "/project/extension/component-foo", "pathname": "/project/extension/component-foo",
"query": Object { "query": Object {
"id": "foo", "id": "foo",
"qualifier": "TRK",
}, },
} }
} }
"pathname": "/project/extension/component-bar", "pathname": "/project/extension/component-bar",
"query": Object { "query": Object {
"id": "foo", "id": "foo",
"qualifier": "TRK",
}, },
} }
} }
"pathname": "/project/admin/extension/foo", "pathname": "/project/admin/extension/foo",
"query": Object { "query": Object {
"id": "foo", "id": "foo",
"qualifier": "TRK",
}, },
} }
} }
"pathname": "/project/admin/extension/bar", "pathname": "/project/admin/extension/bar",
"query": Object { "query": Object {
"id": "foo", "id": "foo",
"qualifier": "TRK",
}, },
} }
} }

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

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "qwe", "id": "qwe",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }

+ 3
- 1
server/sonar-web/src/main/js/app/types.ts View File

} }


export type HomePage = export type HomePage =
| { type: HomePageType.Application; component: string }
| { type: HomePageType.Application; branch: string | undefined; component: string }
| { type: HomePageType.Issues } | { type: HomePageType.Issues }
| { type: HomePageType.MyIssues } | { type: HomePageType.MyIssues }
| { type: HomePageType.MyProjects } | { type: HomePageType.MyProjects }
assigneeLogin?: string; assigneeLogin?: string;
assigneeName?: string; assigneeName?: string;
author?: string; author?: string;
branch?: string;
comments?: IssueComment[]; comments?: IssueComment[];
component: string; component: string;
componentLongName: string; componentLongName: string;
projectName: string; projectName: string;
projectOrganization: string; projectOrganization: string;
projectUuid: string; projectUuid: string;
pullRequest?: string;
resolution?: string; resolution?: string;
rule: string; rule: string;
ruleName: string; ruleName: string;

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

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }

+ 1
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap View File

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }

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

return <span />; return <span />;
} }


return <Measure value={measure.value} metricKey={finalMetricKey} metricType={finalMetricType} />;
return <Measure metricKey={finalMetricKey} metricType={finalMetricType} value={measure.value} />;
} }

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

import { BranchLike } from '../../../app/types'; import { BranchLike } from '../../../app/types';
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { getBranchLikeQuery } from '../../../helpers/branches'; import { getBranchLikeQuery } from '../../../helpers/branches';
import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon';
import { translate } from '../../../helpers/l10n';


function getTooltip(component: Component) { function getTooltip(component: Component) {
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS';
let inner = null; let inner = null;


if (component.refKey && component.qualifier !== 'SVW') { if (component.refKey && component.qualifier !== 'SVW') {
const branch = rootComponent.qualifier === 'APP' ? { branch: component.branch } : {};
inner = ( inner = (
<Link <Link
to={{ pathname: '/dashboard', query: { id: component.refKey } }}
className="link-with-icon">
className="link-with-icon"
to={{ pathname: '/dashboard', query: { id: component.refKey, ...branch } }}>
<QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span>
</Link> </Link>
); );
Object.assign(query, { selected: component.key }); Object.assign(query, { selected: component.key });
} }
inner = ( inner = (
<Link to={{ pathname: '/code', query }} className="link-with-icon">
<Link className="link-with-icon" to={{ pathname: '/code', query }}>
<QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span>
</Link> </Link>
); );
); );
} }


if (rootComponent.qualifier === 'APP') {
inner = (
<>
{inner}
{component.branch ? (
<>
<LongLivingBranchIcon className="spacer-left little-spacer-right" />
<span className="note">{component.branch}</span>
</>
) : (
<span className="spacer-left outline-badge">{translate('branches.main_branch')}</span>
)}
</>
);
}

return <Truncated title={getTooltip(component)}>{inner}</Truncated>; return <Truncated title={getTooltip(component)}>{inner}</Truncated>;
} }

+ 1
- 0
server/sonar-web/src/main/js/apps/code/types.ts View File

import { Measure } from '../../helpers/measures'; import { Measure } from '../../helpers/measures';


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

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

import { Link } from 'react-router'; import { Link } from 'react-router';
import LinkIcon from '../../../components/icons-components/LinkIcon'; import LinkIcon from '../../../components/icons-components/LinkIcon';
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon';
import { splitPath } from '../../../helpers/path'; import { splitPath } from '../../../helpers/path';
import { import {
getPathUrlAsString, getPathUrlAsString,
getBranchLikeUrl, getBranchLikeUrl,
getLongLivingBranchUrl,
getComponentDrilldownUrlWithSelection getComponentDrilldownUrlWithSelection
} from '../../../helpers/urls'; } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
/*:: import type { Component, ComponentEnhanced } from '../types'; */ /*:: import type { Component, ComponentEnhanced } from '../types'; */
/*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: import type { Metric } from '../../../store/metrics/actions'; */


const { component } = this.props; const { component } = this.props;
let head = ''; let head = '';
let tail = component.name; let tail = component.name;
let branch = null;


if (['DIR', 'FIL', 'UTS'].includes(component.qualifier)) { if (['DIR', 'FIL', 'UTS'].includes(component.qualifier)) {
const parts = splitPath(component.path); const parts = splitPath(component.path);
({ head, tail } = parts); ({ head, tail } = parts);
} }

if (this.props.rootComponent.qualifier === 'APP') {
branch = (
<React.Fragment>
{component.branch ? (
<React.Fragment>
<LongLivingBranchIcon className="spacer-left little-spacer-right" />
<span className="note">{component.branch}</span>
</React.Fragment>
) : (
<span className="spacer-left outline-badge">{translate('branches.main_branch')}</span>
)}
</React.Fragment>
);
}
return ( return (
<span title={component.refKey || component.key}> <span title={component.refKey || component.key}>
<QualifierIcon qualifier={component.qualifier} /> <QualifierIcon qualifier={component.qualifier} />
&nbsp; &nbsp;
{head.length > 0 && <span className="note">{head}/</span>} {head.length > 0 && <span className="note">{head}/</span>}
<span>{tail}</span> <span>{tail}</span>
{branch}
</span> </span>
); );
} }


render() { render() {
const { branchLike, component, metric, rootComponent } = this.props; const { branchLike, component, metric, rootComponent } = this.props;
const to =
this.props.rootComponent.qualifier === 'APP'
? getLongLivingBranchUrl(component.refKey, component.branch)
: getBranchLikeUrl(component.refKey, branchLike);
return ( return (
<td className="measure-details-component-cell"> <td className="measure-details-component-cell">
<div className="text-ellipsis"> <div className="text-ellipsis">
<Link <Link
className="link-no-underline" className="link-no-underline"
id={'component-measures-component-link-' + component.key} id={'component-measures-component-link-' + component.key}
to={getBranchLikeUrl(component.refKey, branchLike)}>
to={to}>
<span className="big-spacer-right"> <span className="big-spacer-right">
<LinkIcon /> <LinkIcon />
</span> </span>

+ 2
- 12
server/sonar-web/src/main/js/apps/component/components/App.tsx View File

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; import * as React from 'react';
import { PullRequest, BranchType, ShortLivingBranch } from '../../../app/types';
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import { fillBranchLike } from '../../../helpers/branches';


interface Props { interface Props {
location: { location: {
// TODO find a way to avoid creating this fakeBranchLike // TODO find a way to avoid creating this fakeBranchLike
// probably the best way would be to drop this page completely // probably the best way would be to drop this page completely
// and redirect to the Code page // and redirect to the Code page
let fakeBranchLike: ShortLivingBranch | PullRequest | undefined = undefined;
if (branch) {
fakeBranchLike = {
isMain: false,
mergeBranch: '',
name: branch,
type: BranchType.SHORT
} as ShortLivingBranch;
} else if (pullRequest) {
fakeBranchLike = { base: '', branch: '', key: pullRequest, title: '' } as PullRequest;
}
const fakeBranchLike = fillBranchLike(branch, pullRequest);


return ( return (
<div className="page page-limited"> <div className="page page-limited">

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

isShortLivingBranch, isShortLivingBranch,
isSameBranchLike, isSameBranchLike,
getBranchLikeQuery, getBranchLikeQuery,
isPullRequest
isPullRequest,
fillBranchLike
} from '../../../helpers/branches'; } from '../../../helpers/branches';
import { translate, translateWithParameters } from '../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../helpers/l10n';
import { RawQuery } from '../../../helpers/query'; import { RawQuery } from '../../../helpers/query';
<div> <div>
{openIssue ? ( {openIssue ? (
<IssuesSourceViewer <IssuesSourceViewer
branchLike={this.props.branchLike}
branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
loadIssues={this.fetchIssuesForComponent} loadIssues={this.fetchIssuesForComponent}
locationsNavigator={this.state.locationsNavigator} locationsNavigator={this.state.locationsNavigator}
onIssueChange={this.handleIssueChange} onIssueChange={this.handleIssueChange}

+ 3
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap View File

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "proj", "id": "proj",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "proj", "id": "proj",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "sub-proj", "id": "sub-proj",
}, },
} }

+ 8
- 3
server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.tsx View File

import { getApplicationLeak } from '../../../api/application'; import { getApplicationLeak } from '../../../api/application';
import { translate, translateWithParameters } from '../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../helpers/l10n';
import DateFromNow from '../../../components/intl/DateFromNow'; import DateFromNow from '../../../components/intl/DateFromNow';
import { LightComponent, LongLivingBranch } from '../../../app/types';


interface Props { interface Props {
component: string;
branch?: LongLivingBranch;
component: LightComponent;
} }


interface State { interface State {
} }


componentWillReceiveProps(nextProps: Props) { componentWillReceiveProps(nextProps: Props) {
if (nextProps.component !== this.props.component) {
if (nextProps.component.key !== this.props.component.key) {
this.setState({ leaks: undefined }); this.setState({ leaks: undefined });
} }
} }


fetchLeaks = () => { fetchLeaks = () => {
if (!this.state.leaks) { if (!this.state.leaks) {
getApplicationLeak(this.props.component).then(
getApplicationLeak(
this.props.component.key,
this.props.branch ? this.props.branch.name : undefined
).then(
leaks => { leaks => {
if (this.mounted) { if (this.mounted) {
this.setState({ this.setState({

+ 9
- 2
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx View File

PROJECT_ACTIVITY_GRAPH, PROJECT_ACTIVITY_GRAPH,
PROJECT_ACTIVITY_GRAPH_CUSTOM PROJECT_ACTIVITY_GRAPH_CUSTOM
} from '../../projectActivity/utils'; } from '../../projectActivity/utils';
import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
import {
isSameBranchLike,
getBranchLikeQuery,
isLongLivingBranch
} from '../../../helpers/branches';
import { fetchMetrics } from '../../../store/rootActions'; import { fetchMetrics } from '../../../store/rootActions';
import { getMetrics } from '../../../store/rootReducer'; import { getMetrics } from '../../../store/rootReducer';
import { BranchLike, Component, Metric } from '../../../app/types'; import { BranchLike, Component, Metric } from '../../../app/types';
return ( return (
<div className="overview-main page-main"> <div className="overview-main page-main">
{component.qualifier === 'APP' ? ( {component.qualifier === 'APP' ? (
<ApplicationQualityGate component={component} />
<ApplicationQualityGate
branch={isLongLivingBranch(branchLike) ? branchLike : undefined}
component={component}
/>
) : ( ) : (
<QualityGate branchLike={branchLike} component={component} measures={measures} /> <QualityGate branchLike={branchLike} component={component} measures={measures} />
)} )}

+ 5
- 1
server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.tsx View File

})); }));


it('renders', async () => { it('renders', async () => {
const wrapper = shallow(<ApplicationLeakPeriodLegend component="foo" />);
const wrapper = shallow(
<ApplicationLeakPeriodLegend
component={{ key: 'foo', organization: 'bar', qualifier: 'APP' }}
/>
);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();


await waitAndUpdate(wrapper); await waitAndUpdate(wrapper);

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

import { getMetricName } from '../helpers/metrics'; import { getMetricName } from '../helpers/metrics';
import { getComponentDrilldownUrl } from '../../../helpers/urls'; import { getComponentDrilldownUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { isLongLivingBranch } from '../../../helpers/branches';


export class BugsAndVulnerabilities extends React.PureComponent<ComposedProps> { export class BugsAndVulnerabilities extends React.PureComponent<ComposedProps> {
renderHeader() { renderHeader() {
const { branchLike, component } = this.props; const { branchLike, component } = this.props;

return ( return (
<div className="overview-card-header"> <div className="overview-card-header">
<div className="overview-title"> <div className="overview-title">
} }


renderLeak() { renderLeak() {
const { component, leakPeriod } = this.props;
const { branchLike, component, leakPeriod } = this.props;
if (!leakPeriod) { if (!leakPeriod) {
return null; return null;
} }
return ( return (
<div className="overview-domain-leak"> <div className="overview-domain-leak">
{component.qualifier === 'APP' ? ( {component.qualifier === 'APP' ? (
<ApplicationLeakPeriodLegend component={component.key} />
<ApplicationLeakPeriodLegend
branch={isLongLivingBranch(branchLike) ? branchLike : undefined}
component={component}
/>
) : ( ) : (
<LeakPeriodLegend period={leakPeriod} /> <LeakPeriodLegend period={leakPeriod} />
)} )}

+ 4
- 2
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx View File

import Level from '../../../components/ui/Level'; import Level from '../../../components/ui/Level';
import { getApplicationQualityGate, ApplicationProject } from '../../../api/quality-gates'; import { getApplicationQualityGate, ApplicationProject } from '../../../api/quality-gates';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { LightComponent, Metric } from '../../../app/types';
import { LightComponent, Metric, LongLivingBranch } from '../../../app/types';
import DocTooltip from '../../../components/docs/DocTooltip'; import DocTooltip from '../../../components/docs/DocTooltip';


interface Props { interface Props {
branch?: LongLivingBranch;
component: LightComponent; component: LightComponent;
} }


} }


fetchDetails = () => { fetchDetails = () => {
const { component } = this.props;
const { branch, component } = this.props;
this.setState({ loading: true }); this.setState({ loading: true });
getApplicationQualityGate({ getApplicationQualityGate({
application: component.key, application: component.key,
branch: branch ? branch.name : undefined,
organization: component.organization organization: component.organization
}).then( }).then(
({ status, projects, metrics }) => { ({ status, projects, metrics }) => {

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

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }

+ 3
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap View File

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "barbar", "id": "barbar",
}, },
} }
Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "bazbaz", "id": "bazbaz",
}, },
} }

+ 1
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap View File

"link": Object { "link": Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
}, },

+ 1
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap View File

"link": Object { "link": Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "foo", "id": "foo",
}, },
}, },

+ 1
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap View File

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "name", "id": "name",
}, },
} }

+ 1
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap View File

Object { Object {
"pathname": "/dashboard", "pathname": "/dashboard",
"query": Object { "query": Object {
"branch": undefined,
"id": "project:src/file.js", "id": "project:src/file.js",
}, },
} }

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

return {}; return {};
} }
} }

// Create branch object from branch name or pull request key
export function fillBranchLike(
branch?: string,
pullRequest?: string
): ShortLivingBranch | PullRequest | undefined {
if (branch) {
return {
isMain: false,
mergeBranch: '',
name: branch,
type: BranchType.SHORT
} as ShortLivingBranch;
} else if (pullRequest) {
return { base: '', branch: '', key: pullRequest, title: '' } as PullRequest;
}
return undefined;
}

+ 5
- 3
server/sonar-web/src/main/js/helpers/urls.ts View File

return 'https://sonarcloud.io' + getPathUrlAsString(location); return 'https://sonarcloud.io' + getPathUrlAsString(location);
} }


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


export function getPortfolioUrl(key: string): Location { export function getPortfolioUrl(key: string): Location {
export function getHomePageUrl(homepage: HomePage) { export function getHomePageUrl(homepage: HomePage) {
switch (homepage.type) { switch (homepage.type) {
case HomePageType.Application: case HomePageType.Application:
return getProjectUrl(homepage.component);
return homepage.branch
? getProjectUrl(homepage.component, homepage.branch)
: getProjectUrl(homepage.component);
case HomePageType.Project: case HomePageType.Project:
return homepage.branch return homepage.branch
? getLongLivingBranchUrl(homepage.component, homepage.branch) ? getLongLivingBranchUrl(homepage.component, homepage.branch)

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

project_deletion.page.description=Delete this project. The operation cannot be undone. project_deletion.page.description=Delete this project. The operation cannot be undone.
portfolio_deletion.page.description=This portfolio and its sub-portfolios will be deleted. If this portfolio is referenced by other entities, it will be removed from them. Independent entities referenced by this portfolio, such as projects and other top-level portfolios will not be deleted. This operation cannot be undone. portfolio_deletion.page.description=This portfolio and its sub-portfolios will be deleted. If this portfolio is referenced by other entities, it will be removed from them. Independent entities referenced by this portfolio, such as projects and other top-level portfolios will not be deleted. This operation cannot be undone.
application_deletion.page.description=Delete this application. Application projects will not be deleted. Projects referenced by this application will not be deleted. This operation cannot be undone. application_deletion.page.description=Delete this application. Application projects will not be deleted. Projects referenced by this application will not be deleted. This operation cannot be undone.
application.branches.help=Easily create Application branches composed of the branches of projects in your application.
application.branches.link=Create Branch
project_branches.page=Branches & Pull Requests project_branches.page=Branches & Pull Requests
project_branches.page.description=Use this page to manage project branches and pull requests. project_branches.page.description=Use this page to manage project branches and pull requests.
project_branches.page.life_time=Short-lived branches and pull requests are permanently deleted after {days} days without analysis. project_branches.page.life_time=Short-lived branches and pull requests are permanently deleted after {days} days without analysis.

Loading…
Cancel
Save